mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 14:19:39 +02:00
Add new category flow (#1857)
* resolve git issue * Add new category flow * Improve contrast checker * make error message small * update ui to match figma design * realign color picker * changes * rename color picker controller to new category controller * cleanup code * cleanup code * resize and realign icon avatar * Fix js lint errors Signed-off-by: Syed Bariman Jan <syedbarimanjan@gmail.com> --------- Signed-off-by: Syed Bariman Jan <syedbarimanjan@gmail.com>
This commit is contained in:
parent
ac9703031f
commit
95989a6c9b
14 changed files with 335 additions and 40 deletions
206
app/javascript/controllers/category_controller.js
Normal file
206
app/javascript/controllers/category_controller.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import Pickr from '@simonwep/pickr'
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["pickerBtn", "colorInput", "colorsSection", "paletteSection", "pickerSection", "colorPreview", "avatar", "details", "icon","validationMessage","selection","colorPickerRadioBtn"];
|
||||
static values = {
|
||||
presetColors: Array,
|
||||
};
|
||||
|
||||
initialize() {
|
||||
this.pickerBtnTarget.addEventListener('click', () => {
|
||||
this.showPaletteSection();
|
||||
});
|
||||
|
||||
this.colorInputTarget.addEventListener('input', (e) => {
|
||||
this.picker.setColor(e.target.value);
|
||||
});
|
||||
|
||||
this.detailsTarget.addEventListener('toggle', (e) => {
|
||||
if (!this.colorInputTarget.checkValidity()) {
|
||||
e.preventDefault();
|
||||
this.colorInputTarget.reportValidity();
|
||||
e.target.open = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.selectedIcon = null;
|
||||
|
||||
if (!this.presetColorsValue.includes(this.colorInputTarget.value)) {
|
||||
this.colorPickerRadioBtnTarget.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
initPicker() {
|
||||
const pickerContainer = document.createElement("div");
|
||||
pickerContainer.classList.add("pickerContainer");
|
||||
this.pickerSectionTarget.append(pickerContainer);
|
||||
|
||||
this.picker = Pickr.create({
|
||||
el: this.pickerBtnTarget,
|
||||
theme: 'monolith',
|
||||
container: ".pickerContainer",
|
||||
useAsButton: true,
|
||||
showAlways: true,
|
||||
default: this.colorInputTarget.value,
|
||||
components: {
|
||||
hue: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.picker.on('change', (color) => {
|
||||
const hexColor = color.toHEXA().toString();
|
||||
const rgbacolor = color.toRGBA();
|
||||
|
||||
this.updateAvatarColors(hexColor);
|
||||
this.updateSelectedIconColor(hexColor);
|
||||
|
||||
const backgroundColor = this.backgroundColor(rgbacolor, 10);
|
||||
const contrastRatio = this.contrast(rgbacolor, backgroundColor);
|
||||
|
||||
this.colorInputTarget.value = hexColor;
|
||||
this.colorInputTarget.dataset.colorPickerColorValue = hexColor;
|
||||
this.colorPreviewTarget.style.backgroundColor = hexColor;
|
||||
|
||||
this.handleContrastValidation(contrastRatio);
|
||||
});
|
||||
}
|
||||
|
||||
updateAvatarColors(color) {
|
||||
this.avatarTarget.style.backgroundColor = `${this.#backgroundColor(color)}`;
|
||||
this.avatarTarget.style.color = color;
|
||||
}
|
||||
|
||||
handleIconColorChange(e) {
|
||||
const selectedIcon = e.target;
|
||||
this.selectedIcon = selectedIcon;
|
||||
|
||||
const currentColor = this.colorInputTarget.value;
|
||||
|
||||
this.iconTargets.forEach(icon => {
|
||||
const iconWrapper = icon.nextElementSibling;
|
||||
iconWrapper.style.removeProperty("background-color")
|
||||
iconWrapper.style.color = "black";
|
||||
});
|
||||
|
||||
this.updateSelectedIconColor(currentColor);
|
||||
}
|
||||
|
||||
handleIconChange(e) {
|
||||
const iconSVG = e.currentTarget.closest('label').querySelector('svg').cloneNode(true);
|
||||
this.avatarTarget.innerHTML = '';
|
||||
iconSVG.style.padding = "0px"
|
||||
iconSVG.classList.add("w-8","h-8")
|
||||
this.avatarTarget.appendChild(iconSVG);
|
||||
}
|
||||
|
||||
updateSelectedIconColor(color) {
|
||||
if (this.selectedIcon) {
|
||||
const iconWrapper = this.selectedIcon.nextElementSibling;
|
||||
iconWrapper.style.backgroundColor = `${this.#backgroundColor(color)}`;
|
||||
iconWrapper.style.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
handleColorChange(e) {
|
||||
const color = e.currentTarget.value;
|
||||
this.colorInputTarget.value = color;
|
||||
this.colorPreviewTarget.style.backgroundColor = color;
|
||||
this.updateAvatarColors(color);
|
||||
this.updateSelectedIconColor(color);
|
||||
}
|
||||
|
||||
handleContrastValidation(contrastRatio) {
|
||||
if (contrastRatio < 4.5) {
|
||||
this.colorInputTarget.setCustomValidity("Poor contrast, choose darker color or auto-adjust.");
|
||||
|
||||
this.validationMessageTarget.classList.remove("hidden");
|
||||
} else {
|
||||
this.colorInputTarget.setCustomValidity("");
|
||||
this.validationMessageTarget.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
autoAdjust(e){
|
||||
const currentRGBA = this.picker.getColor();
|
||||
const adjustedRGBA = this.darkenColor(currentRGBA).toString();
|
||||
this.picker.setColor(adjustedRGBA);
|
||||
}
|
||||
|
||||
handleParentChange(e) {
|
||||
const parent = e.currentTarget.value;
|
||||
const display = typeof parent === "string" && parent !== "" ? "none" : "flex";
|
||||
this.selectionTarget.style.display = display;
|
||||
}
|
||||
|
||||
backgroundColor([r,g,b,a], percentage) {
|
||||
const mixedR = Math.round((r * (percentage / 100)) + (255 * (1 - percentage / 100)));
|
||||
const mixedG = Math.round((g * (percentage / 100)) + (255 * (1 - percentage / 100)));
|
||||
const mixedB = Math.round((b * (percentage / 100)) + (255 * (1 - percentage / 100)));
|
||||
return [mixedR, mixedG, mixedB];
|
||||
}
|
||||
|
||||
luminance([r,g,b]) {
|
||||
const toLinear = c => {
|
||||
const scaled = c / 255;
|
||||
return scaled <= 0.04045
|
||||
? scaled / 12.92
|
||||
: ((scaled + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
||||
}
|
||||
|
||||
contrast(foregroundColor, backgroundColor) {
|
||||
const fgLum = this.luminance(foregroundColor);
|
||||
const bgLum = this.luminance(backgroundColor);
|
||||
const [l1, l2] = [Math.max(fgLum, bgLum), Math.min(fgLum, bgLum)];
|
||||
return (l1 + 0.05) / (l2 + 0.05);
|
||||
}
|
||||
|
||||
darkenColor(color) {
|
||||
let darkened = color.toRGBA();
|
||||
const backgroundColor = this.backgroundColor(darkened, 10);
|
||||
let contrastRatio = this.contrast(darkened, backgroundColor);
|
||||
|
||||
while (contrastRatio < 4.5 && (darkened[0] > 0 || darkened[1] > 0 || darkened[2] > 0)) {
|
||||
darkened = [
|
||||
Math.max(0, darkened[0] - 10),
|
||||
Math.max(0, darkened[1] - 10),
|
||||
Math.max(0, darkened[2] - 10),
|
||||
darkened[3]
|
||||
];
|
||||
contrastRatio = this.contrast(darkened, backgroundColor);
|
||||
}
|
||||
|
||||
return `rgba(${darkened.join(", ")})`;
|
||||
}
|
||||
|
||||
showPaletteSection() {
|
||||
this.initPicker();
|
||||
this.colorsSectionTarget.classList.add('hidden');
|
||||
this.paletteSectionTarget.classList.remove('hidden');
|
||||
this.pickerSectionTarget.classList.remove('hidden');
|
||||
this.picker.show();
|
||||
}
|
||||
|
||||
showColorsSection() {
|
||||
this.colorsSectionTarget.classList.remove('hidden');
|
||||
this.paletteSectionTarget.classList.add('hidden');
|
||||
this.pickerSectionTarget.classList.add('hidden');
|
||||
if (this.picker) {
|
||||
this.picker.destroyAndRemove();
|
||||
}
|
||||
}
|
||||
|
||||
toggleSections() {
|
||||
if (this.colorsSectionTarget.classList.contains('hidden')) {
|
||||
this.showColorsSection();
|
||||
} else {
|
||||
this.showPaletteSection();
|
||||
}
|
||||
}
|
||||
|
||||
#backgroundColor(color) {
|
||||
return `color-mix(in oklab, ${color} 10%, transparent)`;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue