Merge pull request #31 from AEmotionStudio/palette/editable-color-picker-6542475088779009480

🎨 Palette: Editable Hex Code in Color Picker
This commit is contained in:
Æmotion Studio
2026-02-09 19:57:30 -08:00
committed by GitHub
5 changed files with 136 additions and 6 deletions
+15
View File
@@ -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;
}
+35 -3
View File
@@ -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);
});
+48
View File
@@ -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();
});
});
});
+15
View File
@@ -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;
}
+23 -3
View File
@@ -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", () => {