From de3fa100d11c31576bfe657d7c19700f6f7f482a Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Tue, 26 Aug 2025 14:03:36 -0500 Subject: [PATCH] Improve directory modal with keyboard navigation and README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add arrow key navigation for autosuggestion dropdown - Add scroll-into-view for long suggestion lists - Add enter key selection for highlighted suggestions - Add README with macOS CIFS share access instructions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 26 +++++++++ src/components/DirectoryModal.tsx | 88 ++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf12a13 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Photos Gallery + +A Next.js application for displaying and organizing photos. + +## Prerequisites + +### macOS CIFS Share Access + +If you're running this application on macOS and accessing photos from a mounted CIFS share, you'll need to grant your terminal application full disk access: + +1. Open **System Preferences** → **Security & Privacy** → **Privacy** +2. Select **Full Disk Access** from the left sidebar +3. Click the lock icon and enter your password to make changes +4. Click the **+** button and add your terminal application (e.g., Terminal.app, iTerm2, etc.) +5. Restart your terminal application + +This is required because macOS restricts access to network-mounted drives without explicit permission. + +## Getting Started + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. \ No newline at end of file diff --git a/src/components/DirectoryModal.tsx b/src/components/DirectoryModal.tsx index 79025a7..0bdfd5a 100644 --- a/src/components/DirectoryModal.tsx +++ b/src/components/DirectoryModal.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useCallback, useEffect } from 'react' +import { useState, useCallback, useEffect, useRef } from 'react' import { createPortal } from 'react-dom' import { IconCheck, IconX } from '@tabler/icons-react' import Button from './Button' @@ -17,6 +17,10 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod const [isValid, setIsValid] = useState(null) const [validationTimeout, setValidationTimeout] = useState(null) const [mounted, setMounted] = useState(false) + const [suggestions, setSuggestions] = useState([]) + const [showSuggestions, setShowSuggestions] = useState(false) + const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1) + const suggestionRefs = useRef<(HTMLDivElement | null)[]>([]) useEffect(() => { setMounted(true) @@ -42,6 +46,10 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod const result = await response.json() setIsValid(result.valid) + setSuggestions(result.suggestions || []) + setShowSuggestions(result.suggestions && result.suggestions.length > 0) + setSelectedSuggestionIndex(-1) + suggestionRefs.current = [] } catch (error) { setIsValid(false) } finally { @@ -52,6 +60,8 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value setDirectory(value) + setShowSuggestions(false) // Hide suggestions while typing + setSelectedSuggestionIndex(-1) if (validationTimeout) { clearTimeout(validationTimeout) @@ -64,6 +74,58 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod setValidationTimeout(timeout) } + const handleSuggestionClick = (suggestion: string) => { + setDirectory(suggestion) + setShowSuggestions(false) + setSelectedSuggestionIndex(-1) + validateDirectory(suggestion) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!showSuggestions || suggestions.length === 0) return + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setSelectedSuggestionIndex(prev => { + const newIndex = prev === -1 ? 0 : (prev < suggestions.length - 1 ? prev + 1 : 0) + setTimeout(() => { + suggestionRefs.current[newIndex]?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }) + }, 0) + return newIndex + }) + break + case 'ArrowUp': + e.preventDefault() + setSelectedSuggestionIndex(prev => { + const newIndex = prev > 0 ? prev - 1 : suggestions.length - 1 + setTimeout(() => { + suggestionRefs.current[newIndex]?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }) + }, 0) + return newIndex + }) + break + case 'Enter': + e.preventDefault() + if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < suggestions.length) { + handleSuggestionClick(suggestions[selectedSuggestionIndex]) + } else if (directory.trim() && isValid) { + handleSave() + } + break + case 'Escape': + setShowSuggestions(false) + setSelectedSuggestionIndex(-1) + break + } + } + if (!isOpen || !mounted) return null console.log('Modal is rendering with isOpen:', isOpen) @@ -80,6 +142,9 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod const handleClose = () => { setDirectory('') setIsValid(null) + setSuggestions([]) + setShowSuggestions(false) + setSelectedSuggestionIndex(-1) if (validationTimeout) { clearTimeout(validationTimeout) } @@ -101,6 +166,8 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod type="text" value={directory} onChange={handleInputChange} + onKeyDown={handleKeyDown} + onFocus={() => setShowSuggestions(suggestions.length > 0)} placeholder="/path/to/photos" className="w-full p-2 pr-10 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white" autoFocus @@ -116,6 +183,25 @@ export default function DirectoryModal({ isOpen, onClose, onSave }: DirectoryMod )} + + {showSuggestions && suggestions.length > 0 && ( +
+ {suggestions.map((suggestion, index) => ( +
(suggestionRefs.current[index] = el)} + onClick={() => handleSuggestionClick(suggestion)} + className={`px-3 py-2 cursor-pointer text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-600 last:border-b-0 ${ + index === selectedSuggestionIndex + ? 'bg-blue-100 dark:bg-blue-900' + : 'hover:bg-gray-100 dark:hover:bg-gray-600' + }`} + > + {suggestion} +
+ ))} +
+ )}