Add new category flow #1857
Reference in New Issue
Block a user
Delete Branch "Improvement/new-add-category-flow"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A video showing the changes in action:
https://github.com/user-attachments/assets/ee8b7dae-40f9-493a-9ca2-fb7dc9fb6e0a
Fixes #1734
/claim #1734
This code has been taken from the webaim.orgs contrast checker.
@zachgoll can you please approve the workflow and review.
I think we should update this to:
app/assets/stylesheets/simonweb_pickr.cssand then import it into our mainapplication.csstailwind file:We've made some recent changes to the Tailwind files, so you'll want to pull those in first. After that, we should move this to the
@layer componentsdirective within thetailwind/application.cssfile.Looks like something happened here with the parent change handling:
https://github.com/user-attachments/assets/263d2342-b67d-439e-a5c0-33330e862c4b
I'm a little worried that we're doing too much here. I think if we choose to integrate a 3rd party picker library, we shouldn't need this amount of code to simply choose a color.
I think we need to either:
<input type="color" />(pictured below)cc @justinfar would you be okay with a basic, native color picker like I've shown above if it drastically reduces the complexity of this feature? (we can style the button to open it better than i did here—this was just for an example)
All of the pure/vanilla ja libraries have been made with no customization in which is why we need this much code.
The color library is only providing us the color preview box and the slider beneath it the validation logic is seperate.
This will simplify the code alot but the picker element can not be changed in any way, cant even change where it will open.
Keep in mind that if we even use the built in color picker we would still need the code from line 50 till 119 to validate the contrast.
I would recommend to use the builtin color picker as the custom design isnt needed for this most of the users are used to the builtin input and it works fine.
Would prefer to keep the color picker custom if possible, but if this is causing too much complexity I'm fine with the browser native color picker
Usability concern: Releasing the mouse from outside the modal always closes it, even when the mouse was pressed from inside the modal. Dragging motions to the outside feel common with this UI, so it would be annoying to close unintentionally, especially if there was more information written in the previous window.
https://github.com/user-attachments/assets/bfdae9a5-7f93-4faf-8e58-f5fd9c5cd71e
My thoughts are to make the
modalview close iff the mouse was pressed and released from outside, something along the lines of astopPropagationcall /:stopaction on the<dialog>'s only child and adding a flag to the modal controller that gets set on mouse down.@zachgoll your final comments?
Let's go ahead with the following solution:
pickrlibrary + Stimulus controllerbackgroundColor(rgba),luminance(rgb), andcontrast(luminanceForeground, luminanceBackground)4.5:1, the following should happen:text-xsmessage above saying "Choose a darker color"@goldenstein64 would you mind opening a new issue for this? I agree with your suggestion, but since it applies to all of our modals / drawers in the app, it's probably worth a standalone issue.
@zachgoll we are getting the color in hex format from the user so we will have to convert it to rgb which will make the function count to 4.
The
picker.on('change')event should pass an HSVaColor Object, so I was thinking we can usetoRGBA()for contrast checking, and calltoHEXA()for the actualinputvalue.@zachgoll
https://github.com/user-attachments/assets/2b07b0b6-5e76-4d1f-b7a1-818f6d227c46
Contrast checking looks good!
@@ -1,42 +1,70 @@<%# locals: (category:, categories:) %><div data-controller="color-avatar"><div data-controller="category" data-category-preset-colors-value="<%= Category::COLORS %>">There are a few style mismatches here. In order to clear out the default
styled_forminput styles, you can passinline: truewhich tells our form builder to omit default styles:@@ -17,1 +14,3 @@</div><div class=" absolute z-50 bg-white p-4 border border-alpha-black-25 rounded-2xl shadow-xs h-fit left-66 top-24"><div class="flex gap-2 flex-col mb-4" data-category-target="selection" style="<%= "display:none;" if @category.subcategory? %>"><div data-category-target="pickerSection"></div>We will need to update the structure of the
details/summaryso that we're not opening the details unless the user clicks the edit icon. Something along these lines:https://github.com/user-attachments/assets/3c3a015f-e349-4e21-acd7-d6d5a1e10074
@@ -34,4 +63,3 @@<div class="space-y-2"><%= 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" } %>I think we should also throw a "clear icon" (i.e.
nilradio button) somewhere here so the user can clear the icon if needed.@justinfar I didn't see this in the designs. What is the preferred method for clearing an icon?
Also, the selected icon needs to match the chosen color:

Throughout the codebase, we're using the
color_avatarfor categories, tags, and merchants.Since we're adding specialization to the category avatar, I think we need to revert changes on this "shared" avatar and make a specific partial just for categories.
I think we should avoid observers like this and update this with one of the following approaches:
Validation states look good!
@justinfar can you do a quick check here on the design for us? Any changes you'd like?
We are using 5% for the background in here:
79e1a2c0ff/app/views/shared/_color_avatar.html.erb (L5)Re validation for contrast, if possible I'd prefer if the validation offered a way to automatically adjust (which basically darkens the color to an acceptable contrast ratio)
Text that should be used for validation is:
Poor contrast, choose darker color or auto-adjust.I did think about this but got stuck on which color should I adjust to like should I just make the color black?
@@ -34,4 +63,3 @@<div class="space-y-2"><%= 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" } %>@zachgoll @syedbarimanjan re clearing an icon, for now let's just go with requiring an icon for all categories
The generic fallback icon should be the "shapes" icon (i.e the one below)

@syedbarimanjan I think we can just add a simple function that brings the hue down until it meets the contrast requirement. Something along these lines; we should be able to reuse quite a bit of the existing contrast checking code:
@zachgoll
https://github.com/user-attachments/assets/b55e5f6c-aed4-4529-8e4d-ef6786604995
Overall looking a lot better! Core flow works great.
Before we merge, I think there are a few general "cleanup" items to address (several mentioned in comments):
Model validation / migration
Currently, the
Categoryclass allowsnilfor a category. Per @justinfar comments about theshapesicon being our default, we'll need to:lucide_iconofshapeson theCategorytablelucide_iconEdge Cases in UI
before_create :inherit_color_from_parentin the model, but really, we need this to bebefore_committo cover both create + update flows + hide the color option in the UI if it is editing a subcategory.https://github.com/user-attachments/assets/2da509c8-17e2-4a98-b327-dbce680d61f8
Now that we've got this into one controller, I think we can rename it to something more specific to categories, such as
category_controller.jsorcategory_avatar_controller.jsWe're mixing a good bit of view logic and contrast validation here. I think we can move a lot of this into the template which will simplify and make the controller a bit easier to read.
In the view, add the template:
And in the controller, we would just need something like this:
Then our validation function just becomes:
We have several hardcoded values of
5%,10%, and15%throughout the controller. We should add a private method:And then throughout the controller:
@@ -0,0 +5,4 @@class="w-14 h-14 flex items-center justify-center rounded-full"style="background-color: color-mix(in oklab, <%= category.color %> 10%, transparent); color: <%= category.color %>"><%= lucide_icon(category.lucide_icon, class: "w-8 h-8") %></span>Once we get the NULL constraint set on the category model, we should be able to update this partial to this:
How about
new_category_controller.js@zachgoll
@@ -23,3 +23,3 @@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 needs to be added back to support the color changes for merchants and tags.
This controller should handle both creates/updates so I think
category_controller.jsis more appropriateThis method can be simplified to just this since setting the color will trigger all these reassignments:
Then simply work with the
HSVaColorobject directly indarkenColormethod:Not a huge deal, but I think the more we can pass around the
HSVaColorobject throughout the controller, the more clear and concise the code becomes and the less conversions we have to deal with.We should pass these into the controller as values so that if we ever change these presets in the Category class, we don't have to come here and update this again to preserve functionality:
@@ -0,0 +5,4 @@class="w-14 h-14 flex items-center justify-center rounded-full"style="background-color: color-mix(in oklab, <%= category.color %> 10%, transparent); color: <%= category.color %>"><%= lucide_icon(category.lucide_icon, class: "w-8 h-8") %></span>Make sure the icon size and alpha channel are matching what I've suggested here so we're in parity with the Figma designs (in oklab, transparent, w-8)
@@ -0,0 +20,4 @@WHERE lucide_icon = 'shapes'SQLendendThis migration will fail on our production data which has categories with
nilvalues in this column. We need to first set existing categories to theshapesicon, then we can safely add these NULL changes here.@@ -10,7 +10,7 @@#Looks like this is from a stale migration of some sort. You can run
bin/rails db:migrate:resetto get rid of this.After all comments are addressed and merge conflicts resolved, we'll run the tests and get this merged.
Functionality looks good!
@@ -0,0 +1,206 @@import { Controller } from "@hotwired/stimulus"We can remove this now
@@ -0,0 +5,4 @@class="w-14 h-14 flex items-center justify-center rounded-full"style="background-color: color-mix(in oklab, <%= category.color %> 10%, transparent); color: <%= category.color %>"><%= lucide_icon(category.lucide_icon, class: "w-8 h-8") %></span>@syedbarimanjan can you update this to match the code I posted?
Code looks good! Looks like there is a failing check, so once resolved we're good to merge.
@zachgoll resolved the failing check and merged with main to resolve conflicts.
@syedbarimanjan great, nice work! Let me know if you have any issues with the bounty payment.