diff --git a/css/sidebar.css b/css/sidebar.css index 9be093d..3d622c9 100644 --- a/css/sidebar.css +++ b/css/sidebar.css @@ -381,4 +381,19 @@ font-size: 11px; font-family: monospace; color: var(--descrip-text, #999); + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + padding: 2px 4px; + width: 60px; + transition: all 0.2s; + text-align: center; +} + +.magnify-color-preview:hover, +.magnify-color-preview:focus { + background: rgba(0, 0, 0, 0.2); + border-color: var(--border-color, #444); + color: var(--fg-color, #ddd); + outline: none; } diff --git a/src/sidebar/SidebarSettings.ts b/src/sidebar/SidebarSettings.ts index d40a786..e69c711 100644 --- a/src/sidebar/SidebarSettings.ts +++ b/src/sidebar/SidebarSettings.ts @@ -237,12 +237,44 @@ export function createColorPicker( colorInput.className = 'magnify-color-input'; colorInput.value = value; - const colorPreview = document.createElement('span'); + const colorPreview = document.createElement('input'); + colorPreview.type = 'text'; colorPreview.className = 'magnify-color-preview'; - colorPreview.textContent = value; + colorPreview.value = value; + colorPreview.maxLength = 7; + colorPreview.setAttribute('aria-label', `Hex code for ${label}`); + + // Stop propagation of keys to prevent global hotkeys while typing + colorPreview.addEventListener('keydown', (e) => e.stopPropagation()); + + // Select all on focus for easy copy/paste + colorPreview.addEventListener('focus', () => colorPreview.select()); + + // Sync from text input to color picker + const updateFromText = () => { + let val = colorPreview.value; + if (!val.startsWith('#') && /^[0-9A-Fa-f]{6}$/.test(val)) { + val = '#' + val; + } + + if (/^#[0-9A-Fa-f]{6}$/.test(val)) { + colorPreview.value = val; // Ensure normalized format + colorInput.value = val; + if (onInput) onInput(val); + onChange(val); + } else { + // Revert to last valid value if invalid + colorPreview.value = colorInput.value; + } + }; + + colorPreview.addEventListener('change', updateFromText); + // Blur is handled by change for most cases, but keeping it as a fallback isn't harmful + // However, since change covers commit actions (Enter/Blur), the explicit blur handler is redundant if updateFromText handles reversion. + // Removing explicit blur handler to avoid double-handling, as change event suffices for committing value. colorInput.addEventListener('input', () => { - colorPreview.textContent = colorInput.value; + colorPreview.value = colorInput.value; if (onInput) onInput(colorInput.value); }); diff --git a/tests/unit/SidebarControls.test.ts b/tests/unit/SidebarControls.test.ts index 165c108..62247b4 100644 --- a/tests/unit/SidebarControls.test.ts +++ b/tests/unit/SidebarControls.test.ts @@ -141,5 +141,53 @@ describe('Sidebar Controls', () => { expect(labelEl.getAttribute('for')).toBe(input.id); expect(input.id).toMatch(/^magnify-color-/); }); + + it('should sync text input with color picker', () => { + const onChange = vi.fn(); + const colorRow = createColorPicker('Test Color', '#000000', onChange); + + const colorInput = colorRow.querySelector('input[type="color"]') as HTMLInputElement; + const textInput = colorRow.querySelector('input.magnify-color-preview') as HTMLInputElement; + + expect(textInput).toBeTruthy(); + expect(textInput.value).toBe('#000000'); + + // Simulate typing a valid hex code + textInput.value = '#ffffff'; + textInput.dispatchEvent(new Event('change')); + + expect(colorInput.value).toBe('#ffffff'); + expect(onChange).toHaveBeenCalledWith('#ffffff'); + + // Simulate typing a hex code without hash + textInput.value = 'ff0000'; + textInput.dispatchEvent(new Event('change')); + + expect(textInput.value).toBe('#ff0000'); // Should normalize + expect(colorInput.value).toBe('#ff0000'); + expect(onChange).toHaveBeenCalledWith('#ff0000'); + + // Simulate typing invalid hex code + textInput.value = 'invalid'; + textInput.dispatchEvent(new Event('change')); + + // Should not update color input (keeps previous value) + expect(colorInput.value).toBe('#ff0000'); + // Should revert text input to last valid value immediately on change + expect(textInput.value).toBe('#ff0000'); + }); + + it('should stop keydown propagation on text input', () => { + const onChange = vi.fn(); + const colorRow = createColorPicker('Test Color', '#000000', onChange); + const textInput = colorRow.querySelector('input.magnify-color-preview') as HTMLInputElement; + + const event = new KeyboardEvent('keydown', { key: 'a' }); + const stopPropagationSpy = vi.spyOn(event, 'stopPropagation'); + + textInput.dispatchEvent(event); + + expect(stopPropagationSpy).toHaveBeenCalled(); + }); }); }); diff --git a/web/sidebar.css b/web/sidebar.css index 9be093d..3d622c9 100644 --- a/web/sidebar.css +++ b/web/sidebar.css @@ -381,4 +381,19 @@ font-size: 11px; font-family: monospace; color: var(--descrip-text, #999); + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + padding: 2px 4px; + width: 60px; + transition: all 0.2s; + text-align: center; +} + +.magnify-color-preview:hover, +.magnify-color-preview:focus { + background: rgba(0, 0, 0, 0.2); + border-color: var(--border-color, #444); + color: var(--fg-color, #ddd); + outline: none; } diff --git a/web/sidebar/SidebarSettings.js b/web/sidebar/SidebarSettings.js index bce8c55..024006f 100644 --- a/web/sidebar/SidebarSettings.js +++ b/web/sidebar/SidebarSettings.js @@ -127,11 +127,31 @@ function createColorPicker(label, value, onChange, tooltip, onInput) { colorInput.id = colorId; colorInput.className = "magnify-color-input"; colorInput.value = value; - const colorPreview = document.createElement("span"); + const colorPreview = document.createElement("input"); + colorPreview.type = "text"; colorPreview.className = "magnify-color-preview"; - colorPreview.textContent = value; + colorPreview.value = value; + colorPreview.maxLength = 7; + colorPreview.setAttribute("aria-label", `Hex code for ${label}`); + colorPreview.addEventListener("keydown", (e) => e.stopPropagation()); + colorPreview.addEventListener("focus", () => colorPreview.select()); + const updateFromText = () => { + let val = colorPreview.value; + if (!val.startsWith("#") && /^[0-9A-Fa-f]{6}$/.test(val)) { + val = "#" + val; + } + if (/^#[0-9A-Fa-f]{6}$/.test(val)) { + colorPreview.value = val; + colorInput.value = val; + if (onInput) onInput(val); + onChange(val); + } else { + colorPreview.value = colorInput.value; + } + }; + colorPreview.addEventListener("change", updateFromText); colorInput.addEventListener("input", () => { - colorPreview.textContent = colorInput.value; + colorPreview.value = colorInput.value; if (onInput) onInput(colorInput.value); }); colorInput.addEventListener("change", () => {