From 95989a6c9befa319d6af6908bdc71b805f42be8b Mon Sep 17 00:00:00 2001 From: Syed Bariman Jan Date: Mon, 24 Feb 2025 21:08:05 +0500 Subject: [PATCH] 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 --------- Signed-off-by: Syed Bariman Jan --- app/assets/stylesheets/simonweb_pickr.css | 2 + app/assets/tailwind/application.css | 29 +++ .../controllers/category_controller.js | 206 ++++++++++++++++++ .../controllers/color_avatar_controller.js | 8 +- app/models/category.rb | 4 +- app/models/demo/generator.rb | 6 +- app/views/categories/_color_avatar.html.erb | 8 + app/views/categories/_form.html.erb | 74 +++++-- app/views/shared/_color_avatar.html.erb | 2 +- app/views/shared/_modal.html.erb | 4 +- config/importmap.rb | 1 + ...5_add_default_lucide_icon_to_categories.rb | 23 ++ db/schema.rb | 4 +- vendor/javascript/@simonwep--pickr.js | 4 + 14 files changed, 335 insertions(+), 40 deletions(-) create mode 100644 app/assets/stylesheets/simonweb_pickr.css create mode 100644 app/javascript/controllers/category_controller.js create mode 100644 app/views/categories/_color_avatar.html.erb create mode 100644 db/migrate/20250220200735_add_default_lucide_icon_to_categories.rb create mode 100644 vendor/javascript/@simonwep--pickr.js diff --git a/app/assets/stylesheets/simonweb_pickr.css b/app/assets/stylesheets/simonweb_pickr.css new file mode 100644 index 00000000..c99ab696 --- /dev/null +++ b/app/assets/stylesheets/simonweb_pickr.css @@ -0,0 +1,2 @@ +/*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */ +.pickr{position:relative;overflow:visible;transform:translateY(0)}.pickr *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr .pcr-button{position:relative;height:2em;width:2em;padding:.5em;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;border-radius:.15em;background:url("data:image/svg+xml;utf8, ") no-repeat center;background-size:0;transition:all .3s}.pickr .pcr-button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pickr .pcr-button::before{z-index:initial}.pickr .pcr-button::after{position:absolute;content:"";top:0;left:0;height:100%;width:100%;transition:background .3s;background:var(--pcr-color);border-radius:.15em}.pickr .pcr-button.clear{background-size:70%}.pickr .pcr-button.clear::before{opacity:0}.pickr .pcr-button.clear:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-button.disabled{cursor:not-allowed}.pickr *,.pcr-app *{box-sizing:border-box;outline:none;border:none;-webkit-appearance:none}.pickr input:focus,.pickr input.pcr-active,.pickr button:focus,.pickr button.pcr-active,.pcr-app input:focus,.pcr-app input.pcr-active,.pcr-app button:focus,.pcr-app button.pcr-active{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)}.pickr .pcr-palette,.pickr .pcr-slider,.pcr-app .pcr-palette,.pcr-app .pcr-slider{transition:box-shadow .3s}.pickr .pcr-palette:focus,.pickr .pcr-slider:focus,.pcr-app .pcr-palette:focus,.pcr-app .pcr-slider:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(0,0,0,.25)}.pcr-app{position:fixed;display:flex;flex-direction:column;z-index:10000;border-radius:.1em;background:#fff;opacity:0;visibility:hidden;transition:opacity .3s,visibility 0s .3s;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;box-shadow:0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);left:0;top:0}.pcr-app.visible{transition:opacity .3s;visibility:visible;opacity:1}.pcr-app .pcr-swatches{display:flex;flex-wrap:wrap;margin-top:.75em}.pcr-app .pcr-swatches.pcr-last{margin:0}@supports(display: grid){.pcr-app .pcr-swatches{display:grid;align-items:center;grid-template-columns:repeat(auto-fit, 1.75em)}}.pcr-app .pcr-swatches>button{font-size:1em;position:relative;width:calc(1.75em - 5px);height:calc(1.75em - 5px);border-radius:.15em;cursor:pointer;margin:2.5px;flex-shrink:0;justify-self:center;transition:all .15s;overflow:hidden;background:rgba(0,0,0,0);z-index:1}.pcr-app .pcr-swatches>button::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:6px;border-radius:.15em;z-index:-1}.pcr-app .pcr-swatches>button::after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:var(--pcr-color);border:1px solid rgba(0,0,0,.05);border-radius:.15em;box-sizing:border-box}.pcr-app .pcr-swatches>button:hover{filter:brightness(1.05)}.pcr-app .pcr-swatches>button:not(.pcr-active){box-shadow:none}.pcr-app .pcr-interaction{display:flex;flex-wrap:wrap;align-items:center;margin:0 -0.2em 0 -0.2em}.pcr-app .pcr-interaction>*{margin:0 .2em}.pcr-app .pcr-interaction input{letter-spacing:.07em;font-size:.75em;text-align:center;cursor:pointer;color:#75797e;background:#f1f3f4;border-radius:.15em;transition:all .15s;padding:.45em .5em;margin-top:.75em}.pcr-app .pcr-interaction input:hover{filter:brightness(0.975)}.pcr-app .pcr-interaction input:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(66,133,244,.75)}.pcr-app .pcr-interaction .pcr-result{color:#75797e;text-align:left;flex:1 1 8em;min-width:8em;transition:all .2s;border-radius:.15em;background:#f1f3f4;cursor:text}.pcr-app .pcr-interaction .pcr-result::-moz-selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-result::selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-type.active{color:#fff;background:#4285f4}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff;width:auto}.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear{color:#fff}.pcr-app .pcr-interaction .pcr-save:hover,.pcr-app .pcr-interaction .pcr-cancel:hover,.pcr-app .pcr-interaction .pcr-clear:hover{filter:brightness(0.925)}.pcr-app .pcr-interaction .pcr-save{background:#4285f4}.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-cancel{background:#f44250}.pcr-app .pcr-interaction .pcr-clear:focus,.pcr-app .pcr-interaction .pcr-cancel:focus{box-shadow:0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(244,66,80,.75)}.pcr-app .pcr-selection .pcr-picker{position:absolute;height:18px;width:18px;border:2px solid #fff;border-radius:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pcr-app .pcr-selection .pcr-color-palette,.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity{position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;display:flex;flex-direction:column;cursor:grab;cursor:-webkit-grab}.pcr-app .pcr-selection .pcr-color-palette:active,.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active{cursor:grabbing;cursor:-webkit-grabbing}.pcr-app[data-theme=monolith]{width:14.25em;max-width:95vw;padding:.8em}.pcr-app[data-theme=monolith] .pcr-selection{display:flex;flex-direction:column;justify-content:space-between;flex-grow:1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview{position:relative;z-index:1;width:100%;height:1em;display:flex;flex-direction:row;justify-content:space-between;margin-bottom:.5em}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-last-color{cursor:pointer;transition:background-color .3s,box-shadow .3s;border-radius:.15em 0 0 .15em;z-index:2}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-current-color{border-radius:0 .15em .15em 0}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-last-color,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-preview .pcr-current-color{background:var(--pcr-color);width:50%;height:100%}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-palette{width:100%;height:8em;z-index:1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-palette .pcr-palette{border-radius:.15em;width:100%;height:100%}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-palette .pcr-palette::before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url("data:image/svg+xml;utf8, ");background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity{height:.5em;margin-top:.75em}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity .pcr-picker{top:50%;transform:translateY(-50%)}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity .pcr-slider{flex-grow:1;border-radius:50em}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-chooser .pcr-slider{background:linear-gradient(to right, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(0, 100%, 50%))}.pcr-app[data-theme=monolith] .pcr-selection .pcr-color-opacity .pcr-slider{background:linear-gradient(to right, transparent, black),url("data:image/svg+xml;utf8, ");background-size:100%,.25em} \ No newline at end of file diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index ecb797a1..7159c950 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -8,6 +8,35 @@ @plugin "@tailwindcss/typography"; @plugin "@tailwindcss/forms"; +@import "../stylesheets/simonweb_pickr.css"; + +@layer components { + .pcr-app{ + position: static !important; + background: none !important; + box-shadow: none !important; + padding: 0 !important; + width: 100% !important; + } + .pcr-color-palette{ + height: 12em !important; + width: 21.5rem !important; + } + .pcr-palette{ + border-radius: 10px !important; + } + .pcr-palette:before{ + border-radius: 10px !important; + } + .pcr-color-chooser{ + height: 1.5em !important; + } + .pcr-picker{ + height: 20px !important; + width: 20px !important; + } +} + .combobox { .hw-combobox__main__wrapper, .hw-combobox__input { diff --git a/app/javascript/controllers/category_controller.js b/app/javascript/controllers/category_controller.js new file mode 100644 index 00000000..dfbcd029 --- /dev/null +++ b/app/javascript/controllers/category_controller.js @@ -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)`; + } +} diff --git a/app/javascript/controllers/color_avatar_controller.js b/app/javascript/controllers/color_avatar_controller.js index 41b7568b..49d9caeb 100644 --- a/app/javascript/controllers/color_avatar_controller.js +++ b/app/javascript/controllers/color_avatar_controller.js @@ -21,14 +21,8 @@ export default class extends Controller { handleColorChange(e) { const color = e.currentTarget.value; - this.avatarTarget.style.backgroundColor = `color-mix(in srgb, ${color} 5%, white)`; + this.avatarTarget.style.backgroundColor = `color-mix(in srgb, ${color} 10%, white)`; this.avatarTarget.style.borderColor = `color-mix(in srgb, ${color} 10%, white)`; this.avatarTarget.style.color = color; } - - handleParentChange(e) { - const parent = e.currentTarget.value; - const visibility = typeof parent === "string" && parent !== "" ? "hidden" : "visible" - this.selectionTarget.style.visibility = visibility - } } diff --git a/app/models/category.rb b/app/models/category.rb index 15e8e0bc..2adc7788 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -8,13 +8,13 @@ class Category < ApplicationRecord has_many :subcategories, class_name: "Category", foreign_key: :parent_id, dependent: :nullify belongs_to :parent, class_name: "Category", optional: true - validates :name, :color, :family, presence: true + validates :name, :color, :lucide_icon, :family, presence: true validates :name, uniqueness: { scope: :family_id } validate :category_level_limit validate :nested_category_matches_parent_classification - before_create :inherit_color_from_parent + before_save :inherit_color_from_parent scope :alphabetically, -> { order(:name) } scope :roots, -> { where(parent_id: nil) } diff --git a/app/models/demo/generator.rb b/app/models/demo/generator.rb index bc5036eb..d60be41e 100644 --- a/app/models/demo/generator.rb +++ b/app/models/demo/generator.rb @@ -142,9 +142,9 @@ class Demo::Generator family.categories.bootstrap_defaults food = family.categories.find_by(name: "Food & Drink") - family.categories.create!(name: "Restaurants", parent: food, color: COLORS.sample, classification: "expense") - family.categories.create!(name: "Groceries", parent: food, color: COLORS.sample, classification: "expense") - family.categories.create!(name: "Alcohol & Bars", parent: food, color: COLORS.sample, classification: "expense") + family.categories.create!(name: "Restaurants", parent: food, color: COLORS.sample, lucide_icon: "utensils", classification: "expense") + family.categories.create!(name: "Groceries", parent: food, color: COLORS.sample, lucide_icon: "shopping-cart", classification: "expense") + family.categories.create!(name: "Alcohol & Bars", parent: food, color: COLORS.sample, lucide_icon: "beer", classification: "expense") end def create_merchants!(family) diff --git a/app/views/categories/_color_avatar.html.erb b/app/views/categories/_color_avatar.html.erb new file mode 100644 index 00000000..1c00f365 --- /dev/null +++ b/app/views/categories/_color_avatar.html.erb @@ -0,0 +1,8 @@ +<%# locals: (category:) %> + + + <%= lucide_icon(category.lucide_icon, class: "w-8 h-8") %> + diff --git a/app/views/categories/_form.html.erb b/app/views/categories/_form.html.erb index fd9a8513..81afcab4 100644 --- a/app/views/categories/_form.html.erb +++ b/app/views/categories/_form.html.erb @@ -1,42 +1,70 @@ <%# locals: (category:, categories:) %> -
+
<%= styled_form_with model: category, class: "space-y-4" do |f| %>
- <%= render partial: "shared/color_avatar", locals: { name: category.name, color: category.color } %> + <%= render partial: "color_avatar", locals: { category: category } %>
+
+ + <%= icon("pen", size: "sm") %> + -
- <% Category::COLORS.each do |color| %> - - <% end %> -
+
+
"> +
+

Color

+
+ <% Category::COLORS.each do |color| %> + + <% end %> + +
+ +
+ +
+

Icon

+
+ <% Category.icon_codes.each do |icon| %> + + <% end %> +
+
+
+
<% if category.errors.any? %> <%= render "shared/form_errors", model: category %> <% end %> -
- - <% Category.icon_codes.each do |icon| %> - - <% end %> -
-
<%= f.select :classification, [["Income", "income"], ["Expense", "expense"]], { label: "Classification" }, required: true %> <%= f.text_field :name, placeholder: t(".placeholder"), required: true, autofocus: true, label: "Name", data: { color_avatar_target: "name" } %> <% unless category.parent? %> - <%= f.select :parent_id, categories.pluck(:name, :id), { include_blank: "(unassigned)", label: "Parent category (optional)" }, disabled: category.parent?, data: { action: "change->color-avatar#handleParentChange" } %> + <%= f.select :parent_id, categories.pluck(:name, :id), { include_blank: "(unassigned)", label: "Parent category (optional)" }, disabled: category.parent?, data: { action: "change->category#handleParentChange" } %> <% end %>
diff --git a/app/views/shared/_color_avatar.html.erb b/app/views/shared/_color_avatar.html.erb index be59ad7d..1ce60932 100644 --- a/app/views/shared/_color_avatar.html.erb +++ b/app/views/shared/_color_avatar.html.erb @@ -8,4 +8,4 @@ class="w-8 h-8 flex items-center justify-center rounded-full" style="background-color: <%= background_color %>; border-color: <%= border_color %>; color: <%= color %>"> <%= letter.upcase %> - + \ No newline at end of file diff --git a/app/views/shared/_modal.html.erb b/app/views/shared/_modal.html.erb index 067e4ca0..3468bca7 100644 --- a/app/views/shared/_modal.html.erb +++ b/app/views/shared/_modal.html.erb @@ -1,8 +1,8 @@ <%# locals: (content:, classes:) -%> <%= turbo_frame_tag "modal" do %> - +
<%= content %>
-<% end %> +<% end %> \ No newline at end of file diff --git a/config/importmap.rb b/config/importmap.rb index 65539f9d..f79ddb7f 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -7,6 +7,7 @@ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/services", under: "services", to: "services" pin "@github/hotkey", to: "@github--hotkey.js" # @3.1.0 +pin "@simonwep/pickr", to: "@simonwep--pickr.js" # @1.9.1 # D3 packages pin "d3" # @7.8.5 diff --git a/db/migrate/20250220200735_add_default_lucide_icon_to_categories.rb b/db/migrate/20250220200735_add_default_lucide_icon_to_categories.rb new file mode 100644 index 00000000..d4be6eea --- /dev/null +++ b/db/migrate/20250220200735_add_default_lucide_icon_to_categories.rb @@ -0,0 +1,23 @@ +class AddDefaultLucideIconToCategories < ActiveRecord::Migration[7.2] + def up + execute <<-SQL + UPDATE categories + SET lucide_icon = 'shapes' + WHERE lucide_icon IS NULL + SQL + + change_column_null :categories, :lucide_icon, false + change_column_default :categories, :lucide_icon, 'shapes' + end + + def down + change_column_default :categories, :lucide_icon, nil + change_column_null :categories, :lucide_icon, true + + execute <<-SQL + UPDATE categories + SET lucide_icon = NULL + WHERE lucide_icon = 'shapes' + SQL + end +end diff --git a/db/schema.rb b/db/schema.rb index ccbe329a..d78271ac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_02_20_153958) do +ActiveRecord::Schema[7.2].define(version: 2025_02_20_200735) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -192,7 +192,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_20_153958) do t.datetime "updated_at", null: false t.uuid "parent_id" t.string "classification", default: "expense", null: false - t.string "lucide_icon" + t.string "lucide_icon", default: "shapes", null: false t.index ["family_id"], name: "index_categories_on_family_id" end diff --git a/vendor/javascript/@simonwep--pickr.js b/vendor/javascript/@simonwep--pickr.js new file mode 100644 index 00000000..f0f6e0d2 --- /dev/null +++ b/vendor/javascript/@simonwep--pickr.js @@ -0,0 +1,4 @@ +// @simonwep/pickr@1.9.1 downloaded from https://ga.jspm.io/npm:@simonwep/pickr@1.9.1/dist/pickr.min.js + +var t={};!function(u,h){t=h()}(self,(()=>(()=>{var t={d:(u,h)=>{for(var d in h)t.o(h,d)&&!t.o(u,d)&&Object.defineProperty(u,d,{enumerable:!0,get:h[d]})},o:(t,u)=>Object.prototype.hasOwnProperty.call(t,u),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},u={};t.d(u,{default:()=>E});var h={};function n(t,u,h,d,m={}){u instanceof HTMLCollection||u instanceof NodeList?u=Array.from(u):Array.isArray(u)||(u=[u]),Array.isArray(h)||(h=[h]);for(const S of u)for(const u of h)S[t](u,d,{capture:!1,...m});return Array.prototype.slice.call(arguments,1)}t.r(h),t.d(h,{adjustableInputNumbers:()=>p,createElementFromString:()=>r,createFromTemplate:()=>a,eventPath:()=>l,off:()=>m,on:()=>d,resolveElement:()=>c});const d=n.bind(null,"addEventListener"),m=n.bind(null,"removeEventListener");function r(t){const u=document.createElement("div");return u.innerHTML=t.trim(),u.firstElementChild}function a(t){const e=(t,u)=>{const h=t.getAttribute(u);return t.removeAttribute(u),h},o=(t,u={})=>{const h=e(t,":obj"),d=e(t,":ref"),m=h?u[h]={}:u;d&&(u[d]=t);for(const u of Array.from(t.children)){const t=e(u,":arr"),h=o(u,t?{}:m);t&&(m[t]||(m[t]=[])).push(Object.keys(h).length?h:u)}return u};return o(r(t))}function l(t){let u=t.path||t.composedPath&&t.composedPath();if(u)return u;let h=t.target.parentElement;for(u=[t.target,h];h=h.parentElement;)u.push(h);return u.push(document,window),u}function c(t){return t instanceof Element?t:"string"==typeof t?t.split(/>>/g).reduce(((t,u,h,d)=>(t=t.querySelector(u),ht)){function o(h){const d=[.001,.01,.1][Number(h.shiftKey||2*h.ctrlKey)]*(h.deltaY<0?1:-1);let m=0,S=t.selectionStart;t.value=t.value.replace(/[\d.]+/g,((t,h)=>h<=S&&h+t.length>=S?(S=h,u(Number(t),d,m)):(m++,t))),t.focus(),t.setSelectionRange(S,S),h.preventDefault(),t.dispatchEvent(new Event("input"))}d(t,"focus",(()=>d(window,"wheel",o,{passive:!1}))),d(t,"blur",(()=>m(window,"wheel",o)))}const{min:S,max:L,floor:B,round:P}=Math;function f(t,u,h){u/=100,h/=100;const d=B(t=t/360*6),m=t-d,S=h*(1-u),L=h*(1-m*u),P=h*(1-(1-m)*u),x=d%6;return[255*[h,L,S,S,P,h][x],255*[P,h,h,L,S,S][x],255*[S,S,P,h,h,L][x]]}function v(t,u,h){const d=(2-(u/=100))*(h/=100)/2;return 0!==d&&(u=1===d?0:d<.5?u*h/(2*d):u*h/(2-2*d)),[t,100*u,100*d]}function b(t,u,h){const d=S(t/=255,u/=255,h/=255),m=L(t,u,h),B=m-d;let P,x;if(0===B)P=x=0;else{x=B/m;const d=((m-t)/6+B/2)/B,S=((m-u)/6+B/2)/B,L=((m-h)/6+B/2)/B;t===m?P=L-S:u===m?P=1/3+d-L:h===m&&(P=2/3+S-d),P<0?P+=1:P>1&&(P-=1)}return[360*P,100*x,100*m]}function y(t,u,h,d){u/=100,h/=100;return[...b(255*(1-S(1,(t/=100)*(1-(d/=100))+d)),255*(1-S(1,u*(1-d)+d)),255*(1-S(1,h*(1-d)+d)))]}function g(t,u,h){u/=100;const d=2*(u*=(h/=100)<.5?h:1-h)/(h+u)*100,m=100*(h+u);return[t,isNaN(d)?0:d,m]}function _(t){return b(...t.match(/.{2}/g).map((t=>parseInt(t,16))))}function w(t){t=t.match(/^[a-zA-Z]+$/)?function(t){if("black"===t.toLowerCase())return"#000";const u=document.createElement("canvas").getContext("2d");return u.fillStyle=t,"#000"===u.fillStyle?null:u.fillStyle}(t):t;const u={cmyk:/^cmyk\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)/i,rgba:/^rgba?\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D*?(([\d.]+)(%?)|$)/i,hsla:/^hsla?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i,hsva:/^hsva?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i,hexa:/^#?(([\dA-Fa-f]{3,4})|([\dA-Fa-f]{6})|([\dA-Fa-f]{8}))$/i},o=t=>t.map((t=>/^(|\d+)\.\d+|\d+$/.test(t)?Number(t):void 0));let h;t:for(const d in u)if(h=u[d].exec(t))switch(d){case"cmyk":{const[,t,u,m,S]=o(h);if(t>100||u>100||m>100||S>100)break t;return{values:y(t,u,m,S),type:d}}case"rgba":{let[,t,,u,,m,,,S]=o(h);if(t="%"===h[2]?t/100*255:t,u="%"===h[4]?u/100*255:u,m="%"===h[6]?m/100*255:m,S="%"===h[9]?S/100:S,t>255||u>255||m>255||S<0||S>1)break t;return{values:[...b(t,u,m),S],a:S,type:d}}case"hexa":{let[,t]=h;4!==t.length&&3!==t.length||(t=t.split("").map((t=>t+t)).join(""));const u=t.substring(0,6);let m=t.substring(6);return m=m?parseInt(m,16)/255:void 0,{values:[..._(u),m],a:m,type:d}}case"hsla":{let[,t,u,m,,S]=o(h);if(S="%"===h[6]?S/100:S,t>360||u>100||m>100||S<0||S>1)break t;return{values:[...g(t,u,m),S],a:S,type:d}}case"hsva":{let[,t,u,m,,S]=o(h);if(S="%"===h[6]?S/100:S,t>360||u>100||m>100||S<0||S>1)break t;return{values:[t,u,m,S],a:S,type:d}}}return{values:null,type:null}}function A(t=0,u=0,h=0,d=1){const i=(t,u)=>(h=-1)=>u(~h?t.map((t=>Number(t.toFixed(h)))):t),m={h:t,s:u,v:h,a:d,toHSVA(){const t=[m.h,m.s,m.v,m.a];return t.toString=i(t,(t=>`hsva(${t[0]}, ${t[1]}%, ${t[2]}%, ${m.a})`)),t},toHSLA(){const t=[...v(m.h,m.s,m.v),m.a];return t.toString=i(t,(t=>`hsla(${t[0]}, ${t[1]}%, ${t[2]}%, ${m.a})`)),t},toRGBA(){const t=[...f(m.h,m.s,m.v),m.a];return t.toString=i(t,(t=>`rgba(${t[0]}, ${t[1]}, ${t[2]}, ${m.a})`)),t},toCMYK(){const t=function(t,u,h){const d=f(t,u,h),m=d[0]/255,L=d[1]/255,B=d[2]/255,P=S(1-m,1-L,1-B);return[100*(1===P?0:(1-m-P)/(1-P)),100*(1===P?0:(1-L-P)/(1-P)),100*(1===P?0:(1-B-P)/(1-P)),100*P]}(m.h,m.s,m.v);return t.toString=i(t,(t=>`cmyk(${t[0]}%, ${t[1]}%, ${t[2]}%, ${t[3]}%)`)),t},toHEXA(){const t=function(t,u,h){return f(t,u,h).map((t=>P(t).toString(16).padStart(2,"0")))}(m.h,m.s,m.v),u=m.a>=1?"":Number((255*m.a).toFixed(0)).toString(16).toUpperCase().padStart(2,"0");return u&&t.push(u),t.toString=()=>`#${t.join("").toUpperCase()}`,t},clone:()=>A(m.h,m.s,m.v,m.a)};return m}const $=t=>Math.max(Math.min(t,1),0);function C(t){const u={options:Object.assign({lock:null,onchange:()=>0,onstop:()=>0},t),_keyboard(t){const{options:h}=u,{type:d,key:m}=t;if(document.activeElement===h.wrapper){const{lock:h}=u.options,S="ArrowUp"===m,L="ArrowRight"===m,B="ArrowDown"===m,P="ArrowLeft"===m;if("keydown"===d&&(S||L||B||P)){let d=0,m=0;"v"===h?d=S||L?1:-1:"h"===h?d=S||L?-1:1:(m=S?-1:B?1:0,d=P?-1:L?1:0),u.update($(u.cache.x+.01*d),$(u.cache.y+.01*m)),t.preventDefault()}else m.startsWith("Arrow")&&(u.options.onstop(),t.preventDefault())}},_tapstart(t){d(document,["mouseup","touchend","touchcancel"],u._tapstop),d(document,["mousemove","touchmove"],u._tapmove),t.cancelable&&t.preventDefault(),u._tapmove(t)},_tapmove(t){const{options:h,cache:d}=u,{lock:m,element:S,wrapper:L}=h,B=L.getBoundingClientRect();let P=0,x=0;if(t){const u=t&&t.touches&&t.touches[0];P=t?(u||t).clientX:0,x=t?(u||t).clientY:0,PB.left+B.width&&(P=B.left+B.width),xB.top+B.height&&(x=B.top+B.height),P-=B.left,x-=B.top}else d&&(P=d.x*B.width,x=d.y*B.height);"h"!==m&&(S.style.left=`calc(${P/B.width*100}% - ${S.offsetWidth/2}px)`),"v"!==m&&(S.style.top=`calc(${x/B.height*100}% - ${S.offsetHeight/2}px)`),u.cache={x:P/B.width,y:x/B.height};const R=$(P/B.width),D=$(x/B.height);switch(m){case"v":return h.onchange(R);case"h":return h.onchange(D);default:return h.onchange(R,D)}},_tapstop(){u.options.onstop(),m(document,["mouseup","touchend","touchcancel"],u._tapstop),m(document,["mousemove","touchmove"],u._tapmove)},trigger(){u._tapmove()},update(t=0,h=0){const{left:d,top:m,width:S,height:L}=u.options.wrapper.getBoundingClientRect();"h"===u.options.lock&&(h=t),u._tapmove({clientX:d+S*t,clientY:m+L*h})},destroy(){const{options:t,_tapstart:h,_keyboard:d}=u;m(document,["keydown","keyup"],d),m([t.wrapper,t.element],"mousedown",h),m([t.wrapper,t.element],"touchstart",h,{passive:!1})}},{options:h,_tapstart:S,_keyboard:L}=u;return d([h.wrapper,h.element],"mousedown",S),d([h.wrapper,h.element],"touchstart",S,{passive:!1}),d(document,["keydown","keyup"],L),u}function k(t={}){t=Object.assign({onchange:()=>0,className:"",elements:[]},t);const u=d(t.elements,"click",(u=>{t.elements.forEach((h=>h.classList[u.target===h?"add":"remove"](t.className))),t.onchange(u),u.stopPropagation()}));return{destroy:()=>m(...u)}}const x={variantFlipOrder:{start:"sme",middle:"mse",end:"ems"},positionFlipOrder:{top:"tbrl",right:"rltb",bottom:"btrl",left:"lrbt"},position:"bottom",margin:8,padding:0},O=(t,u,h)=>{const d="object"!=typeof t||t instanceof HTMLElement?{reference:t,popper:u,...h}:t;return{update(t=d){const{reference:u,popper:h}=Object.assign(d,t);if(!h||!u)throw new Error("Popper- or reference-element missing.");return((t,u,h)=>{const{container:d,arrow:m,margin:S,padding:L,position:B,variantFlipOrder:P,positionFlipOrder:R}={container:document.documentElement.getBoundingClientRect(),...x,...h},{left:D,top:H}=u.style;u.style.left="0",u.style.top="0";const j=t.getBoundingClientRect(),F=u.getBoundingClientRect(),N={t:j.top-F.height-S,b:j.bottom+S,r:j.right+S,l:j.left-F.width-S},T={vs:j.left,vm:j.left+j.width/2-F.width/2,ve:j.left+j.width-F.width,hs:j.top,hm:j.bottom-j.height/2-F.height/2,he:j.bottom-F.height},[M,U="middle"]=B.split("-"),V=R[M],z=P[U],{top:I,left:X,bottom:G,right:K}=d;for(const t of V){const h="t"===t||"b"===t;let d=N[t];const[S,B]=h?["top","left"]:["left","top"],[P,x]=h?[F.height,F.width]:[F.width,F.height],[R,D]=h?[G,K]:[K,G],[H,M]=h?[I,X]:[X,I];if(!(dR))for(const R of z){let H=T[(h?"v":"h")+R];if(!(HD)){if(H-=F[B],d-=F[S],u.style[B]=`${H}px`,u.style[S]=`${d}px`,m){const u=h?j.width/2:j.height/2,L=x/2,D=u>L,F=H+{s:D?L:u,m:L,e:D?L:x-u}[R],N=d+{t:P,b:0,r:0,l:P}[t];m.style[B]=`${F}px`,m.style[S]=`${N}px`}return t+R}}}return u.style.left=D,u.style.top=H,null})(u,h,d)}}};class E{static utils=h;static version="1.9.1";static I18N_DEFAULTS={"ui:dialog":"color picker dialog","btn:toggle":"toggle color picker dialog","btn:swatch":"color swatch","btn:last-color":"use previous color","btn:save":"Save","btn:cancel":"Cancel","btn:clear":"Clear","aria:btn:save":"save and close","aria:btn:cancel":"cancel and close","aria:btn:clear":"clear and close","aria:input":"color input field","aria:palette":"color selection area","aria:hue":"hue selection slider","aria:opacity":"selection slider"};static DEFAULT_OPTIONS={appClass:null,theme:"classic",useAsButton:!1,padding:8,disabled:!1,comparison:!0,closeOnScroll:!1,outputPrecision:0,lockOpacity:!1,autoReposition:!0,container:"body",components:{interaction:{}},i18n:{},swatches:null,inline:!1,sliders:null,default:"#42445a",defaultRepresentation:null,position:"bottom-middle",adjustableNumbers:!0,showAlways:!1,closeWithKey:"Escape"};_initializingActive=!0;_recalc=!0;_nanopop=null;_root=null;_color=A();_lastColor=A();_swatchColors=[];_setupAnimationFrame=null;_eventListener={init:[],save:[],hide:[],show:[],clear:[],change:[],changestop:[],cancel:[],swatchselect:[]};constructor(t){this.options=t=Object.assign({...E.DEFAULT_OPTIONS},t);const{swatches:u,components:h,theme:d,sliders:m,lockOpacity:S,padding:L}=t;["nano","monolith"].includes(d)&&!m&&(t.sliders="h"),h.interaction||(h.interaction={});const{preview:B,opacity:P,hue:x,palette:R}=h;h.opacity=!S&&P,h.palette=R||B||P||x,this._preBuild(),this._buildComponents(),this._bindEvents(),this._finalBuild(),u&&u.length&&u.forEach((t=>this.addSwatch(t)));const{button:D,app:H}=this._root;this._nanopop=O(D,H,{margin:L}),D.setAttribute("role","button"),D.setAttribute("aria-label",this._t("btn:toggle"));const j=this;this._setupAnimationFrame=requestAnimationFrame((function e(){if(!H.offsetWidth)return requestAnimationFrame(e);j.setColor(t.default),j._rePositioningPicker(),t.defaultRepresentation&&(j._representation=t.defaultRepresentation,j.setColorRepresentation(j._representation)),t.showAlways&&j.show(),j._initializingActive=!1,j._emit("init")}))}static create=t=>new E(t);_preBuild(){const{options:t}=this;for(const u of["el","container"])t[u]=c(t[u]);this._root=(t=>{const{components:u,useAsButton:h,inline:d,appClass:m,theme:S,lockOpacity:L}=t.options,l=t=>t?"":'style="display:none" hidden',c=u=>t._t(u),B=a(`\n
\n\n ${h?"":''}\n\n
\n
\n
\n \n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n\n
\n\n
\n \n\n \n \n \n \n \n\n \n \n \n
\n
\n
\n `),P=B.interaction;return P.options.find((t=>!t.hidden&&!t.classList.add("active"))),P.type=()=>P.options.find((t=>t.classList.contains("active"))),B})(this),t.useAsButton&&(this._root.button=t.el),t.container.appendChild(this._root.root)}_finalBuild(){const t=this.options,u=this._root;if(t.container.removeChild(u.root),t.inline){const h=t.el.parentElement;t.el.nextSibling?h.insertBefore(u.app,t.el.nextSibling):h.appendChild(u.app)}else t.container.appendChild(u.app);t.useAsButton?t.inline&&t.el.remove():t.el.parentNode.replaceChild(u.root,t.el),t.disabled&&this.disable(),t.comparison||(u.button.style.transition="none",t.useAsButton||(u.preview.lastColor.style.transition="none")),this.hide()}_buildComponents(){const t=this,u=this.options.components,h=(t.options.sliders||"v").repeat(2),[d,m]=h.match(/^[vh]+$/g)?h:[],s=()=>this._color||(this._color=this._lastColor.clone()),S={palette:C({element:t._root.palette.picker,wrapper:t._root.palette.palette,onstop:()=>t._emit("changestop","slider",t),onchange(h,d){if(!u.palette)return;const m=s(),{_root:S,options:L}=t,{lastColor:B,currentColor:P}=S.preview;t._recalc&&(m.s=100*h,m.v=100-100*d,m.v<0&&(m.v=0),t._updateOutput("slider"));const x=m.toRGBA().toString(0);this.element.style.background=x,this.wrapper.style.background=`\n linear-gradient(to top, rgba(0, 0, 0, ${m.a}), transparent),\n linear-gradient(to left, hsla(${m.h}, 100%, 50%, ${m.a}), rgba(255, 255, 255, ${m.a}))\n `,L.comparison?L.useAsButton||t._lastColor||B.style.setProperty("--pcr-color",x):(S.button.style.setProperty("--pcr-color",x),S.button.classList.remove("clear"));const R=m.toHEXA().toString();for(const{el:u,color:h}of t._swatchColors)u.classList[R===h.toHEXA().toString()?"add":"remove"]("pcr-active");P.style.setProperty("--pcr-color",x)}}),hue:C({lock:"v"===m?"h":"v",element:t._root.hue.picker,wrapper:t._root.hue.slider,onstop:()=>t._emit("changestop","slider",t),onchange(h){if(!u.hue||!u.palette)return;const d=s();t._recalc&&(d.h=360*h),this.element.style.backgroundColor=`hsl(${d.h}, 100%, 50%)`,S.palette.trigger()}}),opacity:C({lock:"v"===d?"h":"v",element:t._root.opacity.picker,wrapper:t._root.opacity.slider,onstop:()=>t._emit("changestop","slider",t),onchange(h){if(!u.opacity||!u.palette)return;const d=s();t._recalc&&(d.a=Math.round(100*h)/100),this.element.style.background=`rgba(0, 0, 0, ${d.a})`,S.palette.trigger()}}),selectable:k({elements:t._root.interaction.options,className:"active",onchange(u){t._representation=u.target.getAttribute("data-type").toUpperCase(),t._recalc&&t._updateOutput("swatch")}})};this._components=S}_bindEvents(){const{_root:t,options:u}=this,h=[d(t.interaction.clear,"click",(()=>this._clearColor())),d([t.interaction.cancel,t.preview.lastColor],"click",(()=>{this.setHSVA(...(this._lastColor||this._color).toHSVA(),!0),this._emit("cancel")})),d(t.interaction.save,"click",(()=>{!this.applyColor()&&!u.showAlways&&this.hide()})),d(t.interaction.result,["keyup","input"],(t=>{this.setColor(t.target.value,!0)&&!this._initializingActive&&(this._emit("change",this._color,"input",this),this._emit("changestop","input",this)),t.stopImmediatePropagation()})),d(t.interaction.result,["focus","blur"],(t=>{this._recalc="blur"===t.type,this._recalc&&this._updateOutput(null)})),d([t.palette.palette,t.palette.picker,t.hue.slider,t.hue.picker,t.opacity.slider,t.opacity.picker],["mousedown","touchstart"],(()=>this._recalc=!0),{passive:!0})];if(!u.showAlways){const m=u.closeWithKey;h.push(d(t.button,"click",(()=>this.isOpen()?this.hide():this.show())),d(document,"keyup",(t=>this.isOpen()&&(t.key===m||t.code===m)&&this.hide())),d(document,["touchstart","mousedown"],(u=>{this.isOpen()&&!l(u).some((u=>u===t.app||u===t.button))&&this.hide()}),{capture:!0}))}if(u.adjustableNumbers){const u={rgba:[255,255,255,1],hsva:[360,100,100,1],hsla:[360,100,100,1],cmyk:[100,100,100,100]};p(t.interaction.result,((t,h,d)=>{const m=u[this.getColorRepresentation().toLowerCase()];if(m){const u=m[d],S=t+(u>=100?1e3*h:h);return S<=0?0:Number((S{m.isOpen()&&(u.closeOnScroll&&m.hide(),null===t?(t=setTimeout((()=>t=null),100),requestAnimationFrame((function e(){m._rePositioningPicker(),null!==t&&requestAnimationFrame(e)}))):(clearTimeout(t),t=setTimeout((()=>t=null),100)))}),{capture:!0}))}this._eventBindings=h}_rePositioningPicker(){const{options:t}=this;if(!t.inline&&!this._nanopop.update({container:document.body.getBoundingClientRect(),position:t.position})){const t=this._root.app,u=t.getBoundingClientRect();t.style.top=(window.innerHeight-u.height)/2+"px",t.style.left=(window.innerWidth-u.width)/2+"px"}}_updateOutput(t){const{_root:u,_color:h,options:d}=this;if(u.interaction.type()){const t=`to${u.interaction.type().getAttribute("data-type")}`;u.interaction.result.value="function"==typeof h[t]?h[t]().toString(d.outputPrecision):""}!this._initializingActive&&this._recalc&&this._emit("change",h,t,this)}_clearColor(t=!1){const{_root:u,options:h}=this;h.useAsButton||u.button.style.setProperty("--pcr-color","rgba(0, 0, 0, 0.15)"),u.button.classList.add("clear"),h.showAlways||this.hide(),this._lastColor=null,this._initializingActive||t||(this._emit("save",null),this._emit("clear"))}_parseLocalColor(t){const{values:u,type:h,a:d}=w(t),{lockOpacity:m}=this.options,S=void 0!==d&&1!==d;return u&&3===u.length&&(u[3]=void 0),{values:!u||m&&S?null:u,type:h}}_t(t){return this.options.i18n[t]||E.I18N_DEFAULTS[t]}_emit(t,...u){this._eventListener[t].forEach((t=>t(...u,this)))}on(t,u){return this._eventListener[t].push(u),this}off(t,u){const h=this._eventListener[t]||[],d=h.indexOf(u);return~d&&h.splice(d,1),this}addSwatch(t){const{values:u}=this._parseLocalColor(t);if(u){const{_swatchColors:t,_root:h}=this,m=A(...u),S=r(`