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 %>
+
+
+
+
+
+ <%= f.text_field :color , data: { category_target: "colorInput"}, class: "form-field__input blah", inline: true %>
+ <%= lucide_icon "palette", class: "w-8 h-8 cursor-pointer hover:bg-gray-100 p-1", data: { action: "click->category#toggleSections" } %>
+
+
+ Poor contrast, choose darker color or
+
+
+
+
+
+
+
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 %>
-