diff --git a/CLAUDE.md b/CLAUDE.md index 2fa789d..fe325f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview -This is a Chrome Extension (Manifest V3) that allows users to customize Tana's CSS styling through a popup interface. The extension includes a professional CSS editor with syntax highlighting, linting, auto-completion, and real-time preview. +This is a Chrome Extension (Manifest V3) that allows users to customize Tana's CSS styling through a popup interface. The extension includes a professional CSS editor with syntax highlighting, intelligent auto-completion with type-ahead filtering, real-time linting, code formatting, and live preview functionality. ## Commands @@ -27,8 +27,8 @@ Load the extension in Chrome: - `content.css` - Default content styles ### Key Components -- **CSS Editor**: Professional editor with linting, auto-completion, and formatting -- **Preset System**: Built-in themes (dark, larger text, compact view, custom colors) +- **CSS Editor**: Professional editor with syntax highlighting, linting, and intelligent auto-completion +- **Smart Autocomplete**: Advanced CSS property suggestions with type-ahead filtering and keyboard navigation - **Storage**: Uses Chrome's sync storage API for persistence - **Content Script Injection**: Only runs on `https://app.tana.inc/*` @@ -43,8 +43,51 @@ Load the extension in Chrome: - `activeTab`, `scripting`, `tabs` - For content script injection - Host permission: `https://app.tana.inc/*` only +## Enhanced Autocomplete System + +### CSS Property Suggestions +- **Trigger**: `Cmd+K` (macOS) or `Ctrl+K` (Windows/Linux) +- **Alternative**: `Ctrl+Space` for cross-platform compatibility +- **Properties**: 60+ common CSS properties including animations, flexbox, grid, and transforms + +### Smart Filtering Modes +1. **Prefix Match** (highest priority): `"back"` → background, background-color, background-image +2. **Contains Match** (medium priority): `"size"` → font-size, background-size, box-sizing +3. **Fuzzy Match** (lowest priority): `"ani"` → animation, animation-delay, animation-duration + +### Keyboard Navigation +- **↑/↓ Arrow Keys**: Navigate through suggestions +- **Enter**: Insert selected property with colon +- **Escape**: Close suggestion panel +- **Type-ahead**: Continue typing to filter suggestions in real-time +- **Mouse**: Click or hover to select suggestions + +### Visual Features +- **Highlighted Matches**: Matching characters are visually highlighted +- **Smart Sorting**: Most relevant suggestions appear first +- **Professional UI**: Modern design with gradient header and keyboard hints +- **Auto-positioning**: Panel positioned intelligently near cursor +- **Auto-hide**: Panel closes after 15 seconds of inactivity + +## Recent Major Changes + +### v1.1 - Enhanced Autocomplete & Simplified UI +- **Removed**: Preset system (dark theme, larger text, compact view, custom colors) +- **Added**: Intelligent CSS property autocomplete with type-ahead filtering +- **Enhanced**: Professional suggestion panel with keyboard navigation +- **Improved**: Real-time filtering and smart matching algorithms +- **Focused**: Extension now concentrates on custom CSS editing rather than presets + +### Rationale for Changes +- **Simplified User Experience**: Removed preset clutter to focus on core CSS editing +- **Professional Workflow**: Added VS Code-like autocomplete for better developer experience +- **Flexibility**: Users can write any CSS rather than being limited to preset options +- **Performance**: Streamlined codebase with ~125 lines of preset code removed + ## Key Implementation Details - Uses Chrome Storage sync API for cross-device persistence - Content script listens for storage changes and message passing - CSS injection is handled via dynamic style element creation -- Extension only activates on Tana domains for security \ No newline at end of file +- Extension only activates on Tana domains for security +- Intelligent suggestion panel with efficient content updates and timer management +- Focused architecture prioritizing custom CSS editing over preset functionality \ No newline at end of file diff --git a/package.json b/package.json index e643c71..e776cc6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tana-css-customizer", - "version": "1.0.0", - "description": "A powerful Chrome extension that allows you to customize the appearance of Tana with custom CSS. Features a professional code editor with syntax highlighting, linting, auto-completion, and real-time preview.", + "version": "1.1.0", + "description": "A powerful Chrome extension that allows you to customize the appearance of Tana with custom CSS. Features a professional code editor with syntax highlighting, intelligent auto-completion with type-ahead filtering, real-time linting, code formatting, and live preview.", "main": "popup.js", "scripts": { "build": "echo 'Chrome extension - no build required'", @@ -24,8 +24,11 @@ "productivity", "browser-extension", "code-editor", + "intelligent-autocomplete", + "type-ahead", + "syntax-highlighting", "linting", - "auto-completion" + "code-formatting" ], "author": "Lisa Ross ", "license": "MIT", diff --git a/popup.html b/popup.html index 0390fba..ccc6b37 100644 --- a/popup.html +++ b/popup.html @@ -285,7 +285,9 @@

How to Use:

  • Your CSS is auto-saved as you type
  • Tab / Shift+Tab to indent/outdent lines
  • Ctrl/Cmd+L to format CSS
  • -
  • Ctrl/Cmd+K or Ctrl+Space for property suggestions
  • +
  • Ctrl/Cmd+K or Ctrl+Space for smart CSS property suggestions
  • +
  • Type-ahead filtering: type "ani" → see animation properties
  • +
  • Navigate suggestions with ↑↓ arrows, insert with ⏎ Enter
  • Auto-closes braces { } and maintains indentation
  • Real-time linting shows errors and warnings
  • Drag the resize handle (bottom-right corner) to resize popup
  • diff --git a/popup.js b/popup.js index c27950f..0b024d3 100644 --- a/popup.js +++ b/popup.js @@ -265,6 +265,61 @@ cssEditor.addEventListener('click', (e) => { cssEditor.addEventListener('keydown', (e) => { console.log('Key pressed:', e.key); + // Handle suggestion panel navigation + const activeSuggestionPanel = document.getElementById('suggestion-panel'); + if (activeSuggestionPanel && activeSuggestionPanel.parentNode) { + console.log('Suggestion panel is active, handling key:', e.key); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + console.log('ArrowDown: current index:', selectedSuggestionIndex, 'max:', currentSuggestions.length - 1); + selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, currentSuggestions.length - 1); + console.log('Moving down to index:', selectedSuggestionIndex); + updateSuggestionSelection(); + return; + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + console.log('ArrowUp: current index:', selectedSuggestionIndex, 'min: 0'); + selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, 0); + console.log('Moving up to index:', selectedSuggestionIndex); + updateSuggestionSelection(); + return; + } + + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + if (selectedSuggestionIndex >= 0 && selectedSuggestionIndex < currentSuggestions.length) { + const selectedSuggestion = currentSuggestions[selectedSuggestionIndex]; + console.log('Inserting suggestion:', selectedSuggestion); + insertSuggestion(selectedSuggestion, currentWord); + closeSuggestionPanel(); + } + return; + } + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + closeSuggestionPanel(); + return; + } + + // For typing keys, update suggestions instead of closing panel + if (e.key.length === 1 || e.key === 'Backspace' || e.key === 'Delete') { + console.log('Text modification key pressed:', e.key, '- will update suggestions after text changes'); + // Don't close panel, let the input event handler update suggestions + // Use setTimeout to wait for the text to actually change + setTimeout(() => { + updateSuggestionsFromCurrentText(); + }, 10); + } + } + // Handle Tab key for indentation if (e.key === 'Tab') { e.preventDefault(); @@ -559,15 +614,29 @@ function formatCSS() { } // CSS Property suggestions -function showPropertySuggestions() { - const cssProperties = [ - 'align-items', 'animation', 'background', 'background-color', 'border', 'border-radius', - 'box-shadow', 'color', 'cursor', 'display', 'flex', 'flex-direction', 'font-family', - 'font-size', 'font-weight', 'grid', 'height', 'justify-content', 'line-height', - 'margin', 'max-width', 'opacity', 'overflow', 'padding', 'position', 'text-align', - 'text-decoration', 'transform', 'transition', 'visibility', 'width', 'z-index' - ]; - +let suggestionPanel = null; +let selectedSuggestionIndex = -1; +let currentSuggestions = []; +let currentWord = ''; +let autoHideTimeout = null; + +// CSS properties list +const CSS_PROPERTIES = [ + 'align-items', 'align-content', 'animation', 'animation-delay', 'animation-duration', + 'background', 'background-color', 'background-image', 'background-size', 'background-position', + 'border', 'border-radius', 'border-color', 'border-style', 'border-width', + 'box-shadow', 'box-sizing', 'color', 'content', 'cursor', 'display', 'flex', 'flex-direction', + 'flex-wrap', 'flex-grow', 'flex-shrink', 'font-family', 'font-size', 'font-weight', + 'font-style', 'grid', 'grid-template-columns', 'grid-template-rows', 'grid-gap', + 'height', 'justify-content', 'justify-items', 'line-height', 'list-style', 'margin', + 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'max-width', 'max-height', + 'min-width', 'min-height', 'opacity', 'outline', 'overflow', 'overflow-x', 'overflow-y', + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'position', + 'text-align', 'text-decoration', 'text-shadow', 'text-transform', 'transform', 'transition', + 'transition-duration', 'transition-property', 'visibility', 'white-space', 'width', 'z-index' +]; + +function getCurrentWordAndPosition() { const start = cssEditor.selectionStart; const value = cssEditor.value; const lineStart = value.lastIndexOf('\n', start - 1) + 1; @@ -575,86 +644,374 @@ function showPropertySuggestions() { const words = currentLine.trim().split(/\s+/); const lastWord = words[words.length - 1] || ''; - // Filter properties that match the current typing - const suggestions = cssProperties.filter(prop => - prop.toLowerCase().startsWith(lastWord.toLowerCase()) - ); + return { lastWord, start, value, lineStart, currentLine }; +} + +function filterAndSortSuggestions(searchTerm) { + // Filter properties that match the current typing (type-ahead support) + const suggestions = CSS_PROPERTIES.filter(prop => { + const propLower = prop.toLowerCase(); + const searchLower = searchTerm.toLowerCase(); + + // Exact prefix match gets highest priority + if (propLower.startsWith(searchLower)) { + return true; + } + + // Contains match (type-ahead functionality) + if (propLower.includes(searchLower)) { + return true; + } + + // Fuzzy match for abbreviations (e.g., "ani" matches "animation") + const searchChars = searchLower.split(''); + let propIndex = 0; + for (const char of searchChars) { + const foundIndex = propLower.indexOf(char, propIndex); + if (foundIndex === -1) return false; + propIndex = foundIndex + 1; + } + return true; + }).sort((a, b) => { + const aLower = a.toLowerCase(); + const bLower = b.toLowerCase(); + const searchLower = searchTerm.toLowerCase(); + + // Sort by relevance: + // 1. Exact prefix matches first + if (aLower.startsWith(searchLower) && !bLower.startsWith(searchLower)) return -1; + if (!aLower.startsWith(searchLower) && bLower.startsWith(searchLower)) return 1; + + // 2. Then by contains matches (closer to start is better) + if (aLower.includes(searchLower) && bLower.includes(searchLower)) { + const aIndex = aLower.indexOf(searchLower); + const bIndex = bLower.indexOf(searchLower); + if (aIndex !== bIndex) return aIndex - bIndex; + } + + // 3. Finally alphabetical + return a.localeCompare(b); + }); + + return suggestions; +} + +function showPropertySuggestions() { + const { lastWord } = getCurrentWordAndPosition(); + const suggestions = filterAndSortSuggestions(lastWord); if (suggestions.length > 0) { + currentSuggestions = suggestions; + currentWord = lastWord; + selectedSuggestionIndex = 0; // Select first item by default + console.log('Setting up suggestions:', suggestions.length, 'items, selected index:', selectedSuggestionIndex); showSuggestionPanel(suggestions, lastWord); } } -function showSuggestionPanel(suggestions, currentWord) { - // Remove existing suggestion panel - const existing = document.getElementById('suggestion-panel'); - if (existing) existing.remove(); +function updateSuggestionsFromCurrentText() { + console.log('Updating suggestions from current text...'); + const { lastWord } = getCurrentWordAndPosition(); + + if (!lastWord || lastWord.length === 0) { + console.log('No word to search for, closing panel'); + closeSuggestionPanel(); + return; + } + + const suggestions = filterAndSortSuggestions(lastWord); + console.log('Found', suggestions.length, 'suggestions for:', lastWord); + + if (suggestions.length > 0) { + currentSuggestions = suggestions; + currentWord = lastWord; + selectedSuggestionIndex = 0; // Reset to first item + + // If panel exists, update it; otherwise create new one + const existingPanel = document.getElementById('suggestion-panel'); + if (existingPanel) { + console.log('Updating existing panel content'); + updateSuggestionPanelContent(suggestions, lastWord); + } else { + console.log('Creating new panel'); + showSuggestionPanel(suggestions, lastWord); + } + } else { + console.log('No suggestions found, closing panel'); + closeSuggestionPanel(); + } +} + +function updateSuggestionPanelContent(suggestions, word) { + const panel = document.getElementById('suggestion-panel'); + if (!panel) return; + + // Find the suggestions container (everything between header and footer) + const header = panel.querySelector('div:first-child'); + const footer = panel.querySelector('div:last-child'); + + // Remove all existing suggestion items + const existingItems = panel.querySelectorAll('.suggestion-item'); + existingItems.forEach(item => item.remove()); + + // Add new suggestions + suggestions.slice(0, 12).forEach((suggestion, index) => { + const item = document.createElement('div'); + item.className = 'suggestion-item'; + item.dataset.index = index; + item.style.cssText = ` + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + transition: all 0.15s ease; + display: flex; + align-items: center; + font-weight: 400; + `; + + // Highlight matching characters + item.innerHTML = highlightMatch(suggestion, word); + + // Highlight selected item + if (index === selectedSuggestionIndex) { + item.style.backgroundColor = '#667eea'; + item.style.color = 'white'; + } + + item.addEventListener('mouseenter', () => { + selectedSuggestionIndex = index; + updateSuggestionSelection(); + }); + + item.addEventListener('click', () => { + insertSuggestion(suggestion, word); + closeSuggestionPanel(); + }); + + // Insert before footer + panel.insertBefore(item, footer); + }); + + // Reset auto-hide timer since we're actively using the panel + resetAutoHideTimer(); +} + +function highlightMatch(text, searchTerm) { + if (!searchTerm || searchTerm.length === 0) { + return text; + } + + const lowerText = text.toLowerCase(); + const lowerSearch = searchTerm.toLowerCase(); + + // For exact substring matches, highlight the whole match + if (lowerText.includes(lowerSearch)) { + const index = lowerText.indexOf(lowerSearch); + const before = text.substring(0, index); + const match = text.substring(index, index + searchTerm.length); + const after = text.substring(index + searchTerm.length); + return `${before}${match}${after}`; + } + + // For fuzzy matches, highlight individual characters + let result = ''; + let searchIndex = 0; + const searchChars = lowerSearch.split(''); + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (searchIndex < searchChars.length && char.toLowerCase() === searchChars[searchIndex]) { + result += `${char}`; + searchIndex++; + } else { + result += char; + } + } + + return result; +} + +function showSuggestionPanel(suggestions, word) { + // Remove existing suggestion panel but don't reset state yet + if (suggestionPanel && suggestionPanel.parentNode) { + suggestionPanel.remove(); + } - const panel = document.createElement('div'); - panel.id = 'suggestion-panel'; - panel.style.cssText = ` + suggestionPanel = document.createElement('div'); + suggestionPanel.id = 'suggestion-panel'; + suggestionPanel.style.cssText = ` position: absolute; background: white; - border: 1px solid #ccc; - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - max-height: 150px; + border: 1px solid #667eea; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15), 0 2px 8px rgba(0,0,0,0.1); + max-height: 200px; overflow-y: auto; z-index: 1000; - font-size: 12px; + font-size: 13px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + min-width: 200px; + backdrop-filter: blur(8px); + `; + + // Add header + const header = document.createElement('div'); + header.textContent = 'CSS Properties'; + header.style.cssText = ` + padding: 8px 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 11px; + font-weight: 600; + border-radius: 7px 7px 0 0; + text-transform: uppercase; + letter-spacing: 0.5px; `; + suggestionPanel.appendChild(header); - suggestions.slice(0, 10).forEach((suggestion, index) => { + // Add suggestions with highlighting + suggestions.slice(0, 12).forEach((suggestion, index) => { const item = document.createElement('div'); - item.textContent = suggestion; + item.className = 'suggestion-item'; + item.dataset.index = index; item.style.cssText = ` - padding: 4px 8px; + padding: 8px 12px; cursor: pointer; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #f0f0f0; + transition: all 0.15s ease; + display: flex; + align-items: center; + font-weight: 400; `; - item.addEventListener('mouseenter', () => { - item.style.backgroundColor = '#f0f0f0'; - }); + // Highlight matching characters + item.innerHTML = highlightMatch(suggestion, word); - item.addEventListener('mouseleave', () => { - item.style.backgroundColor = ''; + // Highlight selected item + if (index === selectedSuggestionIndex) { + item.style.backgroundColor = '#667eea'; + item.style.color = 'white'; + } + + item.addEventListener('mouseenter', () => { + selectedSuggestionIndex = index; + updateSuggestionSelection(); }); item.addEventListener('click', () => { - insertSuggestion(suggestion, currentWord); - panel.remove(); + insertSuggestion(suggestion, word); + closeSuggestionPanel(); }); - panel.appendChild(item); + suggestionPanel.appendChild(item); }); - // Position panel near the textarea + // Add footer with keyboard hints + const footer = document.createElement('div'); + footer.innerHTML = '↑↓ Navigate • ⏎ Select • ⎋ Close • Type to filter'; + footer.style.cssText = ` + padding: 6px 12px; + background: #f8f9fa; + color: #666; + font-size: 10px; + border-radius: 0 0 7px 7px; + text-align: center; + border-top: 1px solid #e9ecef; + `; + suggestionPanel.appendChild(footer); + + // Position panel near the cursor const rect = cssEditor.getBoundingClientRect(); - panel.style.left = rect.left + 'px'; - panel.style.top = (rect.top + 20) + 'px'; + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; - document.body.appendChild(panel); + suggestionPanel.style.left = (rect.left + scrollLeft + 10) + 'px'; + suggestionPanel.style.top = (rect.top + scrollTop + 30) + 'px'; - // Auto-hide after 5 seconds - setTimeout(() => { - if (panel.parentNode) panel.remove(); - }, 5000); + document.body.appendChild(suggestionPanel); + + // Set up auto-hide timer (reset any existing timer) + resetAutoHideTimer(); +} + +function updateSuggestionSelection() { + const panel = document.getElementById('suggestion-panel'); + if (!panel) { + console.log('No suggestion panel found for updating selection'); + return; + } + + const items = panel.querySelectorAll('.suggestion-item'); + console.log('Updating selection for index:', selectedSuggestionIndex, 'out of', items.length, 'items'); + + items.forEach((item, index) => { + if (index === selectedSuggestionIndex) { + item.style.backgroundColor = '#667eea'; + item.style.color = 'white'; + item.style.fontWeight = '600'; + item.scrollIntoView({ block: 'nearest' }); + console.log('Selected item:', item.textContent); + } else { + item.style.backgroundColor = ''; + item.style.color = ''; + item.style.fontWeight = '400'; + } + }); +} + +function resetAutoHideTimer() { + // Clear existing timer + if (autoHideTimeout) { + clearTimeout(autoHideTimeout); + } + + // Set new timer (15 seconds for type-ahead) + autoHideTimeout = setTimeout(() => { + console.log('Auto-hiding suggestion panel after timeout'); + closeSuggestionPanel(); + }, 15000); +} + +function closeSuggestionPanel() { + console.log('Closing suggestion panel'); + + // Clear auto-hide timer + if (autoHideTimeout) { + clearTimeout(autoHideTimeout); + autoHideTimeout = null; + } + + if (suggestionPanel && suggestionPanel.parentNode) { + suggestionPanel.remove(); + } + suggestionPanel = null; + selectedSuggestionIndex = -1; + currentSuggestions = []; + currentWord = ''; } -function insertSuggestion(suggestion, currentWord) { +function insertSuggestion(suggestion, word) { const start = cssEditor.selectionStart; const value = cssEditor.value; // Replace the current word with the suggestion - const beforeCursor = value.substring(0, start - currentWord.length); + const beforeCursor = value.substring(0, start - word.length); const afterCursor = value.substring(start); const newValue = beforeCursor + suggestion + ': ' + afterCursor; cssEditor.value = newValue; - cssEditor.setSelectionRange(start - currentWord.length + suggestion.length + 2, start - currentWord.length + suggestion.length + 2); + cssEditor.setSelectionRange(start - word.length + suggestion.length + 2, start - word.length + suggestion.length + 2); cssEditor.focus(); + + // Trigger auto-save + clearTimeout(saveTimeout); + saveTimeout = setTimeout(async () => { + try { + await chrome.storage.sync.set({ customCSS: cssEditor.value }); + } catch (error) { + console.error('Error auto-saving CSS:', error); + } + }, 1000); } // Instructions toggle functionality