1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 21:29:38 +02:00

feature(dark mode): misc design fixes (#2215)

* Fix category dark mode styles

* Fix sidebar account tab states

* Fix dashboard balance sheet group styles

* Fix budget dark mode styles

* Fix chart gradient split

* Fix prose styles in dark mode

* Add back chat nav id for tests
This commit is contained in:
Zach Gollwitzer 2025-05-07 09:26:06 -04:00 committed by GitHub
parent c26a7dd2dd
commit fb7107d614
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 254 additions and 174 deletions

View file

@ -71,18 +71,22 @@
/* Typography */ /* Typography */
.prose { .prose {
@apply max-w-none; @apply max-w-none text-primary;
a {
@apply text-link;
}
h2 { h2 {
@apply text-xl font-medium; @apply text-xl font-medium text-primary;
} }
h3 { h3 {
@apply text-lg font-medium; @apply text-lg font-medium text-primary;
} }
li { li {
@apply m-0; @apply m-0 text-primary;
} }
details { details {

View file

@ -26,6 +26,11 @@
--color-destructive: var(--color-red-600); --color-destructive: var(--color-red-600);
--color-shadow: --alpha(var(--color-black) / 6%); --color-shadow: --alpha(var(--color-black) / 6%);
/* Colors used in Stimulus controllers with SVGs (easier to define light/dark mode here than toggle within the controllers) */
/* See @layer base block below for dark mode overrides */
--budget-unused-fill: var(--color-gray-200);
--budget-unallocated-fill: var(--color-gray-50);
/* Gray scale */ /* Gray scale */
--color-gray-25: #FAFAFA; --color-gray-25: #FAFAFA;
--color-gray-50: #F7F7F7; --color-gray-50: #F7F7F7;
@ -250,6 +255,10 @@
--color-destructive: var(--color-red-400); --color-destructive: var(--color-red-400);
--color-shadow: --alpha(var(--color-white) / 8%); --color-shadow: --alpha(var(--color-white) / 8%);
/* Dark mode overrides for colors used in Stimulus controllers with SVGs */
--budget-unused-fill: var(--color-gray-500);
--budget-unallocated-fill: var(--color-gray-700);
--shadow-xs: 0px 1px 2px 0px --alpha(var(--color-white) / 8%); --shadow-xs: 0px 1px 2px 0px --alpha(var(--color-white) / 8%);
--shadow-sm: 0px 1px 6px 0px --alpha(var(--color-white) / 8%); --shadow-sm: 0px 1px 6px 0px --alpha(var(--color-white) / 8%);
--shadow-md: 0px 4px 8px -2px --alpha(var(--color-white) / 8%); --shadow-md: 0px 4px 8px -2px --alpha(var(--color-white) / 8%);

View file

@ -1,22 +1,36 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus";
import Pickr from '@simonwep/pickr' import Pickr from "@simonwep/pickr";
export default class extends Controller { export default class extends Controller {
static targets = ["pickerBtn", "colorInput", "colorsSection", "paletteSection", "pickerSection", "colorPreview", "avatar", "details", "icon","validationMessage","selection","colorPickerRadioBtn"]; static targets = [
"pickerBtn",
"colorInput",
"colorsSection",
"paletteSection",
"pickerSection",
"colorPreview",
"avatar",
"details",
"icon",
"validationMessage",
"selection",
"colorPickerRadioBtn",
];
static values = { static values = {
presetColors: Array, presetColors: Array,
}; };
initialize() { initialize() {
this.pickerBtnTarget.addEventListener('click', () => { this.pickerBtnTarget.addEventListener("click", () => {
this.showPaletteSection(); this.showPaletteSection();
}); });
this.colorInputTarget.addEventListener('input', (e) => { this.colorInputTarget.addEventListener("input", (e) => {
this.picker.setColor(e.target.value); this.picker.setColor(e.target.value);
}); });
this.detailsTarget.addEventListener('toggle', (e) => { this.detailsTarget.addEventListener("toggle", (e) => {
if (!this.colorInputTarget.checkValidity()) { if (!this.colorInputTarget.checkValidity()) {
e.preventDefault(); e.preventDefault();
this.colorInputTarget.reportValidity(); this.colorInputTarget.reportValidity();
@ -38,7 +52,7 @@ export default class extends Controller {
this.picker = Pickr.create({ this.picker = Pickr.create({
el: this.pickerBtnTarget, el: this.pickerBtnTarget,
theme: 'monolith', theme: "monolith",
container: ".pickerContainer", container: ".pickerContainer",
useAsButton: true, useAsButton: true,
showAlways: true, showAlways: true,
@ -48,7 +62,7 @@ export default class extends Controller {
}, },
}); });
this.picker.on('change', (color) => { this.picker.on("change", (color) => {
const hexColor = color.toHEXA().toString(); const hexColor = color.toHEXA().toString();
const rgbacolor = color.toRGBA(); const rgbacolor = color.toRGBA();
@ -77,20 +91,23 @@ export default class extends Controller {
const currentColor = this.colorInputTarget.value; const currentColor = this.colorInputTarget.value;
this.iconTargets.forEach(icon => { this.iconTargets.forEach((icon) => {
const iconWrapper = icon.nextElementSibling; const iconWrapper = icon.nextElementSibling;
iconWrapper.style.removeProperty("background-color") iconWrapper.style.removeProperty("background-color");
iconWrapper.style.color = "black"; iconWrapper.style.removeProperty("color");
}); });
this.updateSelectedIconColor(currentColor); this.updateSelectedIconColor(currentColor);
} }
handleIconChange(e) { handleIconChange(e) {
const iconSVG = e.currentTarget.closest('label').querySelector('svg').cloneNode(true); const iconSVG = e.currentTarget
this.avatarTarget.innerHTML = ''; .closest("label")
iconSVG.style.padding = "0px" .querySelector("svg")
iconSVG.classList.add("w-8","h-8") .cloneNode(true);
this.avatarTarget.innerHTML = "";
iconSVG.style.padding = "0px";
iconSVG.classList.add("w-8", "h-8");
this.avatarTarget.appendChild(iconSVG); this.avatarTarget.appendChild(iconSVG);
} }
@ -112,7 +129,9 @@ export default class extends Controller {
handleContrastValidation(contrastRatio) { handleContrastValidation(contrastRatio) {
if (contrastRatio < 4.5) { if (contrastRatio < 4.5) {
this.colorInputTarget.setCustomValidity("Poor contrast, choose darker color or auto-adjust."); this.colorInputTarget.setCustomValidity(
"Poor contrast, choose darker color or auto-adjust.",
);
this.validationMessageTarget.classList.remove("hidden"); this.validationMessageTarget.classList.remove("hidden");
} else { } else {
@ -129,19 +148,26 @@ export default class extends Controller {
handleParentChange(e) { handleParentChange(e) {
const parent = e.currentTarget.value; const parent = e.currentTarget.value;
const display = typeof parent === "string" && parent !== "" ? "none" : "flex"; const display =
typeof parent === "string" && parent !== "" ? "none" : "flex";
this.selectionTarget.style.display = display; this.selectionTarget.style.display = display;
} }
backgroundColor([r, g, b, a], percentage) { backgroundColor([r, g, b, a], percentage) {
const mixedR = Math.round((r * (percentage / 100)) + (255 * (1 - percentage / 100))); const mixedR = Math.round(
const mixedG = Math.round((g * (percentage / 100)) + (255 * (1 - percentage / 100))); r * (percentage / 100) + 255 * (1 - percentage / 100),
const mixedB = Math.round((b * (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]; return [mixedR, mixedG, mixedB];
} }
luminance([r, g, b]) { luminance([r, g, b]) {
const toLinear = c => { const toLinear = (c) => {
const scaled = c / 255; const scaled = c / 255;
return scaled <= 0.04045 return scaled <= 0.04045
? scaled / 12.92 ? scaled / 12.92
@ -162,12 +188,15 @@ export default class extends Controller {
const backgroundColor = this.backgroundColor(darkened, 10); const backgroundColor = this.backgroundColor(darkened, 10);
let contrastRatio = this.contrast(darkened, backgroundColor); let contrastRatio = this.contrast(darkened, backgroundColor);
while (contrastRatio < 4.5 && (darkened[0] > 0 || darkened[1] > 0 || darkened[2] > 0)) { while (
contrastRatio < 4.5 &&
(darkened[0] > 0 || darkened[1] > 0 || darkened[2] > 0)
) {
darkened = [ darkened = [
Math.max(0, darkened[0] - 10), Math.max(0, darkened[0] - 10),
Math.max(0, darkened[1] - 10), Math.max(0, darkened[1] - 10),
Math.max(0, darkened[2] - 10), Math.max(0, darkened[2] - 10),
darkened[3] darkened[3],
]; ];
contrastRatio = this.contrast(darkened, backgroundColor); contrastRatio = this.contrast(darkened, backgroundColor);
} }
@ -177,23 +206,23 @@ export default class extends Controller {
showPaletteSection() { showPaletteSection() {
this.initPicker(); this.initPicker();
this.colorsSectionTarget.classList.add('hidden'); this.colorsSectionTarget.classList.add("hidden");
this.paletteSectionTarget.classList.remove('hidden'); this.paletteSectionTarget.classList.remove("hidden");
this.pickerSectionTarget.classList.remove('hidden'); this.pickerSectionTarget.classList.remove("hidden");
this.picker.show(); this.picker.show();
} }
showColorsSection() { showColorsSection() {
this.colorsSectionTarget.classList.remove('hidden'); this.colorsSectionTarget.classList.remove("hidden");
this.paletteSectionTarget.classList.add('hidden'); this.paletteSectionTarget.classList.add("hidden");
this.pickerSectionTarget.classList.add('hidden'); this.pickerSectionTarget.classList.add("hidden");
if (this.picker) { if (this.picker) {
this.picker.destroyAndRemove(); this.picker.destroyAndRemove();
} }
} }
toggleSections() { toggleSections() {
if (this.colorsSectionTarget.classList.contains('hidden')) { if (this.colorsSectionTarget.classList.contains("hidden")) {
this.showColorsSection(); this.showColorsSection();
} else { } else {
this.showPaletteSection(); this.showPaletteSection();

View file

@ -136,13 +136,13 @@ export default class extends Controller {
.attr("fill", function () { .attr("fill", function () {
if (this.dataset.segmentId === segmentId) { if (this.dataset.segmentId === segmentId) {
if (this.dataset.segmentId === unusedSegmentId) { if (this.dataset.segmentId === unusedSegmentId) {
return "#A3A3A3"; return "var(--budget-unused-fill)";
} }
return this.dataset.originalColor; return this.dataset.originalColor;
} }
return "#F0F0F0"; return "var(--budget-unallocated-fill)";
}); });
this.defaultContentTarget.classList.add("hidden"); this.defaultContentTarget.classList.add("hidden");

View file

@ -0,0 +1,16 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="sidebar-tabs"
export default class extends Controller {
static targets = ["account"];
select(event) {
this.accountTargets.forEach((account) => {
if (account.contains(event.target)) {
account.classList.add("bg-container");
} else {
account.classList.remove("bg-container");
}
});
}
}

View file

@ -4,7 +4,6 @@ export default class extends Controller {
static values = { userPreference: String }; static values = { userPreference: String };
connect() { connect() {
this.applyTheme();
this.startSystemThemeListener(); this.startSystemThemeListener();
} }
@ -45,7 +44,7 @@ export default class extends Controller {
if (isDark) { if (isDark) {
document.documentElement.setAttribute("data-theme", "dark"); document.documentElement.setAttribute("data-theme", "dark");
} else { } else {
document.documentElement.removeAttribute("data-theme"); document.documentElement.setAttribute("data-theme", "light");
} }
} }
@ -60,20 +59,12 @@ export default class extends Controller {
} }
}; };
toDark() {
this.setTheme(true);
}
toLight() {
this.setTheme(false);
}
toggle() { toggle() {
const currentTheme = document.documentElement.getAttribute("data-theme"); const currentTheme = document.documentElement.getAttribute("data-theme");
if (currentTheme === "dark") { if (currentTheme === "dark") {
this.toLight(); this.setTheme(false);
} else { } else {
this.toDark(); this.setTheme(true);
} }
} }

View file

@ -138,36 +138,48 @@ export default class extends Controller {
.attr("x1", this._d3XScale.range()[0]) .attr("x1", this._d3XScale.range()[0])
.attr("x2", this._d3XScale.range()[1]); .attr("x2", this._d3XScale.range()[1]);
// First stop - solid trend color
gradient gradient
.append("stop") .append("stop")
.attr("class", "start-color") .attr("class", "start-color")
.attr("offset", "0%") .attr("offset", "0%")
.attr("stop-color", this.dataValue.trend.color); .attr("stop-color", this.dataValue.trend.color);
// Second stop - trend color right before split
gradient gradient
.append("stop") .append("stop")
.attr("class", "middle-color") .attr("class", "split-before")
.attr("offset", "100%") .attr("offset", "100%")
.attr("stop-color", this.dataValue.trend.color); .attr("stop-color", this.dataValue.trend.color);
// Third stop - gray color right after split
gradient
.append("stop")
.attr("class", "split-after")
.attr("offset", "100%")
.attr("stop-color", "var(--color-gray-400)");
// Fourth stop - solid gray to end
gradient gradient
.append("stop") .append("stop")
.attr("class", "end-color") .attr("class", "end-color")
.attr("offset", "100%") .attr("offset", "100%")
.attr("class", "fg-subdued") .attr("stop-color", "var(--color-gray-400)");
.attr("stop-color", "currentColor");
} }
_setTrendlineSplitAt(percent) { _setTrendlineSplitAt(percent) {
const position = percent * 100;
// Update both stops at the split point
this._d3Svg this._d3Svg
.select(`#${this.element.id}-split-gradient`) .select(`#${this.element.id}-split-gradient`)
.select(".middle-color") .select(".split-before")
.attr("offset", `${percent * 100}%`); .attr("offset", `${position}%`);
this._d3Svg this._d3Svg
.select(`#${this.element.id}-split-gradient`) .select(`#${this.element.id}-split-gradient`)
.select(".end-color") .select(".split-after")
.attr("offset", `${percent * 100}%`); .attr("offset", `${position}%`);
this._d3Svg this._d3Svg
.select(`#${this.element.id}-trendline-gradient-rect`) .select(`#${this.element.id}-trendline-gradient-rect`)

View file

@ -131,14 +131,14 @@ class Budget < ApplicationRecord
unused_segment_id = "unused" unused_segment_id = "unused"
# Continuous gray segment for empty budgets # Continuous gray segment for empty budgets
return [ { color: "#F0F0F0", amount: 1, id: unused_segment_id } ] unless allocations_valid? return [ { color: "var(--budget-unallocated-fill)", amount: 1, id: unused_segment_id } ] unless allocations_valid?
segments = budget_categories.map do |bc| segments = budget_categories.map do |bc|
{ color: bc.category.color, amount: budget_category_actual_spending(bc), id: bc.id } { color: bc.category.color, amount: budget_category_actual_spending(bc), id: bc.id }
end end
if available_to_spend.positive? if available_to_spend.positive?
segments.push({ color: "#F0F0F0", amount: available_to_spend, id: unused_segment_id }) segments.push({ color: "var(--budget-unallocated-fill)", amount: available_to_spend, id: unused_segment_id })
end end
segments segments

View file

@ -79,14 +79,14 @@ class BudgetCategory < ApplicationRecord
unused_segment_id = "unused" unused_segment_id = "unused"
overage_segment_id = "overage" overage_segment_id = "overage"
return [ { color: "#F0F0F0", amount: 1, id: unused_segment_id } ] unless actual_spending > 0 return [ { color: "var(--budget-unallocated-fill)", amount: 1, id: unused_segment_id } ] unless actual_spending > 0
segments = [ { color: category.color, amount: actual_spending, id: id } ] segments = [ { color: category.color, amount: actual_spending, id: id } ]
if available_to_spend.negative? if available_to_spend.negative?
segments.push({ color: "#EF4444", amount: available_to_spend.abs, id: overage_segment_id }) segments.push({ color: "var(--color-destructive)", amount: available_to_spend.abs, id: overage_segment_id })
else else
segments.push({ color: "#F0F0F0", amount: available_to_spend, id: unused_segment_id }) segments.push({ color: "var(--budget-unallocated-fill)", amount: available_to_spend, id: unused_segment_id })
end end
segments segments

View file

@ -21,6 +21,7 @@
</details> </details>
<% end %> <% end %>
<div data-controller="sidebar-tabs">
<%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %> <%= render TabsComponent.new(active_tab: active_account_group_tab, url_param_key: "account_group_tab", testid: "account-sidebar-tabs") do |tabs| %>
<% tabs.with_nav do |nav| %> <% tabs.with_nav do |nav| %>
<% nav.with_btn(id: "assets", label: "Assets") %> <% nav.with_btn(id: "assets", label: "Assets") %>
@ -89,3 +90,4 @@
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
</div>

View file

@ -1,6 +1,6 @@
<%# locals: (account_group:) %> <%# locals: (account_group:) %>
<%= render DisclosureComponent.new(title: account_group.name, align: :left) do |disclosure| %> <%= render DisclosureComponent.new(title: account_group.name, align: :left, open: account_group.accounts.any? { |account| page_active?(account_path(account)) }) do |disclosure| %>
<% disclosure.with_summary_content do %> <% disclosure.with_summary_content do %>
<div class="ml-auto text-right grow"> <div class="ml-auto text-right grow">
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %> <%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
@ -15,7 +15,13 @@
<div class="space-y-1"> <div class="space-y-1">
<% account_group.accounts.each do |account| %> <% account_group.accounts.each do |account| %>
<%= link_to account_path(account), class: "block flex items-center gap-2 px-3 py-2 hover:bg-surface-hover", title: account.name do %> <%= link_to account_path(account),
class: class_names(
"block flex items-center gap-2 px-3 py-2 rounded-lg",
page_active?(account_path(account)) ? "bg-container" : "hover:bg-surface-hover"
),
data: { sidebar_tabs_target: "account", action: "click->sidebar-tabs#select" },
title: account.name do %>
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %> <%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
<div class="min-w-0 grow"> <div class="min-w-0 grow">
@ -36,6 +42,7 @@
<% end %> <% end %>
</div> </div>
<div class="my-2">
<%= render LinkComponent.new( <%= render LinkComponent.new(
href: new_polymorphic_path(account_group.key, step: "method_select"), href: new_polymorphic_path(account_group.key, step: "method_select"),
text: "New #{account_group.name.downcase.singularize}", text: "New #{account_group.name.downcase.singularize}",
@ -45,4 +52,5 @@
frame: :modal, frame: :modal,
class: "justify-start" class: "justify-start"
) %> ) %>
</div>
<% end %> <% end %>

View file

@ -15,7 +15,7 @@
<div class="rounded-md h-1.5 bg-green-500" style="width: <%= budget.surplus_percent %>%"></div> <div class="rounded-md h-1.5 bg-green-500" style="width: <%= budget.surplus_percent %>%"></div>
<% else %> <% else %>
<div class="rounded-md h-1.5 bg-green-500" style="width: <%= budget.actual_income_percent %>%"></div> <div class="rounded-md h-1.5 bg-green-500" style="width: <%= budget.actual_income_percent %>%"></div>
<div class="rounded-md h-1.5 bg-gray-100" style="width: <%= 100 - budget.actual_income_percent %>%"></div> <div class="rounded-md h-1.5 bg-surface-inset" style="width: <%= 100 - budget.actual_income_percent %>%"></div>
<% end %> <% end %>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
@ -41,18 +41,18 @@
<div> <div>
<div class="flex h-1.5 mb-3 gap-1"> <div class="flex h-1.5 mb-3 gap-1">
<% if budget.available_to_spend.negative? %> <% if budget.available_to_spend.negative? %>
<div class="rounded-md h-1.5 bg-gray-900" style="width: <%= 100 - budget.overage_percent %>%"></div> <div class="rounded-md h-1.5 bg-inverse" style="width: <%= 100 - budget.overage_percent %>%"></div>
<div class="rounded-md h-1.5 bg-red-500" style="width: <%= budget.overage_percent %>%"></div> <div class="rounded-md h-1.5 bg-destructive" style="width: <%= budget.overage_percent %>%"></div>
<% else %> <% else %>
<div class="rounded-md h-1.5 bg-gray-900" style="width: <%= budget.percent_of_budget_spent %>%"></div> <div class="rounded-md h-1.5 bg-inverse" style="width: <%= budget.percent_of_budget_spent %>%"></div>
<div class="rounded-md h-1.5 bg-gray-100" style="width: <%= 100 - budget.percent_of_budget_spent %>%"></div> <div class="rounded-md h-1.5 bg-surface-inset" style="width: <%= 100 - budget.percent_of_budget_spent %>%"></div>
<% end %> <% end %>
</div> </div>
<div class="flex justify-between text-sm"> <div class="flex justify-between text-sm">
<p class="text-secondary"><%= format_money(budget.actual_spending_money) %> spent</p> <p class="text-secondary"><%= format_money(budget.actual_spending_money) %> spent</p>
<p class="font-medium"> <p class="font-medium">
<% if budget.available_to_spend.negative? %> <% if budget.available_to_spend.negative? %>
<span class="text-red-500"><%= format_money(budget.available_to_spend_money.abs) %> over</span> <span class="text-destructive"><%= format_money(budget.available_to_spend_money.abs) %> over</span>
<% else %> <% else %>
<span class="text-primary"><%= format_money(budget.available_to_spend_money) %> left</span> <span class="text-primary"><%= format_money(budget.available_to_spend_money) %> left</span>
<% end %> <% end %>

View file

@ -16,31 +16,26 @@
</div> </div>
<div> <div>
<% if @budget.initialized? && @budget.available_to_allocate.positive? %> <% if @budget.initialized? && @budget.available_to_allocate.positive? %>
<div class="flex gap-2 mb-2 rounded-lg bg-alpha-black-25 p-1"> <%= render TabsComponent.new(active_tab: params[:tab].presence || "budgeted") do |tabs| %>
<% base_classes = "rounded-md px-2 py-1 flex-1 text-center" %> <% tabs.with_nav do |nav| %>
<% selected_tab = params[:tab].presence || "budgeted" %> <% nav.with_btn(id: "budgeted", label: "Budgeted") %>
<% nav.with_btn(id: "actuals", label: "Actual") %>
<%= link_to "Budgeted", <% end %>
budget_path(@budget, tab: "budgeted"),
class: class_names(
base_classes,
"bg-container shadow-xs text-primary": selected_tab == "budgeted",
"text-secondary": selected_tab != "budgeted"
) %>
<%= link_to "Actual",
budget_path(@budget, tab: "actuals"),
class: class_names(
base_classes,
"bg-container shadow-xs text-primary": selected_tab == "actuals",
"text-secondary": selected_tab != "actuals"
) %>
</div>
<% tabs.with_panel(tab_id: "budgeted") do %>
<div class="bg-container rounded-xl shadow-border-xs"> <div class="bg-container rounded-xl shadow-border-xs">
<%= render selected_tab == "budgeted" ? "budgets/budgeted_summary" : "budgets/actuals_summary", budget: @budget %> <%= render "budgets/budgeted_summary", budget: @budget %>
</div> </div>
<% end %>
<% tabs.with_panel(tab_id: "actuals") do %>
<div class="bg-container rounded-xl shadow-border-xs">
<%= render "budgets/actuals_summary", budget: @budget %>
</div>
<% end %>
<% end %>
<% else %> <% else %>
<div class="bg-container rounded-xl shadow-border-xs"> <div class="bg-container rounded-xl shadow-border-xs">
<%= render "budgets/actuals_summary", budget: @budget %> <%= render "budgets/actuals_summary", budget: @budget %>

View file

@ -2,10 +2,10 @@
<% category ||= Category.uncategorized %> <% category ||= Category.uncategorized %>
<div> <div>
<span class="flex items-center gap-1 text-sm font-medium rounded-full px-1.5 py-1 border truncate" <span class="flex items-center gap-1 text-sm font-medium rounded-full px-1.5 py-1 border truncate focus-visible:outline-none focus-visible:ring-0"
style=" style="
background-color: color-mix(in srgb, <%= category.color %> 5%, white); background-color: color-mix(in oklab, <%= category.color %> 10%, transparent);
border-color: color-mix(in srgb, <%= category.color %> 30%, white); border-color: color-mix(in oklab, <%= category.color %> 10%, transparent);
color: <%= category.color %>;"> color: <%= category.color %>;">
<% if category.lucide_icon.present? %> <% if category.lucide_icon.present? %>
<%= icon category.lucide_icon, size: "sm", color: "current" %> <%= icon category.lucide_icon, size: "sm", color: "current" %>

View file

@ -7,7 +7,7 @@
<%= render partial: "color_avatar", locals: { category: category } %> <%= render partial: "color_avatar", locals: { category: category } %>
<details data-category-target="details"> <details data-category-target="details">
<summary class="cursor-pointer absolute -bottom-2 -right-2 flex justify-center items-center bg-gray-50 border-2 w-7 h-7 border-white rounded-full text-gray-500"> <summary class="cursor-pointer absolute -bottom-2 -right-2 flex justify-center items-center bg-surface-inset hover:bg-surface-inset-hover border-2 w-7 h-7 border-subdued rounded-full text-secondary">
<%= icon("pen", size: "sm") %> <%= icon("pen", size: "sm") %>
</summary> </summary>
@ -19,7 +19,7 @@
<% Category::COLORS.each do |color| %> <% Category::COLORS.each do |color| %>
<label class="relative"> <label class="relative">
<%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->category#handleColorChange" } %> <%= f.radio_button :color, color, class: "sr-only peer", data: { action: "change->category#handleColorChange" } %>
<div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-blue-500" style="background-color: <%= color %>"></div> <div class="w-6 h-6 rounded-full cursor-pointer peer-checked:ring-2 peer-checked:ring-offset-2 peer-checked:ring-gray-500" style="background-color: <%= color %>"></div>
</label> </label>
<% end %> <% end %>
<label class="relative"> <label class="relative">
@ -41,12 +41,12 @@
</div> </div>
<div class="flex flex-wrap gap-2 justify-center flex-col w-87"> <div class="flex flex-wrap gap-2 justify-center flex-col w-87">
<h4 class="text-gray-500 text-sm">Icon</h4> <h4 class="text-secondary text-sm">Icon</h4>
<div class="flex flex-wrap gap-0.5"> <div class="flex flex-wrap gap-0.5">
<% Category.icon_codes.each do |icon| %> <% Category.icon_codes.each do |icon| %>
<label class="relative"> <label class="relative">
<%= f.radio_button :lucide_icon, icon, class: "sr-only peer", data: { action: "change->category#handleIconChange change->category#handleIconColorChange", category_target:"icon" } %> <%= f.radio_button :lucide_icon, icon, class: "sr-only peer", data: { action: "change->category#handleIconChange change->category#handleIconColorChange", category_target:"icon" } %>
<div class="w-7 h-7 flex m-0.5 items-center justify-center rounded-full cursor-pointer hover:bg-gray-100 peer-checked:bg-gray-100 border-1 border-transparent"> <div class="text-secondary w-7 h-7 flex m-0.5 items-center justify-center rounded-full cursor-pointer hover:bg-container-inset-hover peer-checked:bg-container-inset border-1 border-transparent">
<%= icon(icon, size: "sm", color: "current") %> <%= icon(icon, size: "sm", color: "current") %>
</div> </div>
</label> </label>

View file

@ -2,8 +2,8 @@
<% is_selected = category.id === @selected_category&.id %> <% is_selected = category.id === @selected_category&.id %>
<%= content_tag :div, <%= content_tag :div,
class: ["filterable-item flex justify-between items-center border-none rounded-lg px-2 py-1 group w-full hover:bg-gray-25 focus-within:bg-gray-25", class: ["filterable-item flex justify-between items-center border-none rounded-lg px-2 py-1 group w-full hover:bg-container-inset-hover",
{ "bg-gray-25": is_selected }], { "bg-container-inset": is_selected }],
data: { filter_name: category.name } do %> data: { filter_name: category.name } do %>
<%= button_to transaction_category_path( <%= button_to transaction_category_path(
@transaction.entry, @transaction.entry,

View file

@ -2,7 +2,13 @@
<div class="flex flex-col relative" data-controller="list-filter"> <div class="flex flex-col relative" data-controller="list-filter">
<div class="grow p-1.5"> <div class="grow p-1.5">
<div class="relative flex items-center bg-container border border-secondary rounded-lg"> <div class="relative flex items-center bg-container border border-secondary rounded-lg">
<input placeholder="<%= t(".search_placeholder") %>" autocomplete="nope" type="search" class="placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg" data-list-filter-target="input" data-action="list-filter#filter"> <input
placeholder="<%= t(".search_placeholder") %>"
autocomplete="nope"
type="search"
class="bg-container placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0"
data-list-filter-target="input"
data-action="list-filter#filter" />
<%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %> <%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
</div> </div>
</div> </div>
@ -33,14 +39,16 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<hr>
<hr class="border-tertiary">
<div class="relative p-1.5 w-full"> <div class="relative p-1.5 w-full">
<% if @transaction.category %> <% if @transaction.category %>
<%= button_to transaction_path(@transaction.entry), <%= button_to transaction_path(@transaction.entry),
method: :patch, method: :patch,
data: { turbo_frame: dom_id(@transaction.entry) }, data: { turbo_frame: dom_id(@transaction.entry) },
params: { entry: { entryable_type: "Transaction", entryable_attributes: { id: @transaction.id, category_id: nil } } }, params: { entry: { entryable_type: "Transaction", entryable_attributes: { id: @transaction.id, category_id: nil } } },
class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-gray-100" do %> class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-container-inset-hover" do %>
<%= icon("minus") %> <%= icon("minus") %>
<%= t(".clear") %> <%= t(".clear") %>
@ -49,7 +57,7 @@
<% unless @transaction.transfer? %> <% unless @transaction.transfer? %>
<%= link_to new_transaction_transfer_match_path(@transaction.entry), <%= link_to new_transaction_transfer_match_path(@transaction.entry),
class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-gray-100", class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-container-inset-hover",
data: { turbo_frame: "modal" } do %> data: { turbo_frame: "modal" } do %>
<%= icon("refresh-cw") %> <%= icon("refresh-cw") %>

View file

@ -30,8 +30,8 @@
<% questions.each do |question| %> <% questions.each do |question| %>
<button data-action="chat#submitSampleQuestion" <button data-action="chat#submitSampleQuestion"
data-chat-question-param="<%= question[:text] %>" data-chat-question-param="<%= question[:text] %>"
class="w-full flex items-center gap-2 border border-tertiary rounded-full py-1.5 px-2.5 hover:bg-gray-100"> class="w-fit flex items-center gap-2 border border-tertiary rounded-full py-1.5 px-2.5 hover:bg-gray-100">
<%= icon(question[:icon], color: "gray") %> <%= question[:text] %> <%= icon(question[:icon]) %> <%= question[:text] %>
</button> </button>
<% end %> <% end %>
</div> </div>

View file

@ -4,9 +4,12 @@
<% path = chat.new_record? ? chats_path : chats_path(previous_chat_id: chat.id) %> <% path = chat.new_record? ? chats_path : chats_path(previous_chat_id: chat.id) %>
<div class="flex items-center gap-2 grow"> <div class="flex items-center gap-2 grow">
<%= link_to path, id: "chat-nav-back", class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %> <%= render LinkComponent.new(
<%= icon("menu", color: "gray" ) %> id: "chat-nav-back",
<% end %> variant: "icon",
icon: "menu",
href: path,
) %>
<div class="grow"> <div class="grow">
<%= render "chats/chat_title", chat: chat, ctx: "chat" %> <%= render "chats/chat_title", chat: chat, ctx: "chat" %>

View file

@ -4,9 +4,12 @@
<% if @chats.any? %> <% if @chats.any? %>
<nav class="mb-6"> <nav class="mb-6">
<% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %> <% back_path = @last_viewed_chat ? chat_path(@last_viewed_chat) : new_chat_path %>
<%= link_to back_path, class: "w-9 h-9 flex items-center justify-center rounded-lg hover:bg-surface-hover" do %>
<%= icon("arrow-left", color: "gray" ) %> <%= render LinkComponent.new(
<% end %> variant: "icon",
icon: "arrow-left",
href: back_path,
) %>
</nav> </nav>
<% end %> <% end %>

View file

@ -1,5 +1,5 @@
<%= turbo_frame_tag chat_frame do %> <%= turbo_frame_tag chat_frame do %>
<div class="flex flex-col h-full md:p-4"> <div class="flex flex-col h-full md:px-4 md:pb-4">
<%= render "chats/chat_nav", chat: @chat %> <%= render "chats/chat_nav", chat: @chat %>
<div class="mt-auto py-8"> <div class="mt-auto py-8">

View file

@ -30,7 +30,7 @@
</div> </div>
<%# DESKTOP - Chat form %> <%# DESKTOP - Chat form %>
<div class="p-4 lg:mt-auto fixed lg:static left-0 bottom-16 w-full bg-surface"> <div class="p-4 pt-0 lg:mt-auto fixed lg:static left-0 bottom-16 w-full bg-surface">
<%= render "messages/chat_form", chat: @chat %> <%= render "messages/chat_form", chat: @chat %>
</div> </div>
</div> </div>

View file

@ -6,7 +6,7 @@
<%= tag.div class: class_names( <%= tag.div class: class_names(
"w-8 h-8 flex items-center justify-center mx-auto rounded-lg", "w-8 h-8 flex items-center justify-center mx-auto rounded-lg",
active ? "bg-container shadow-xs text-primary" : "group-hover:bg-container-hover text-secondary" active ? "bg-container shadow-xs text-primary" : "group-hover:bg-surface-hover text-secondary"
) do %> ) do %>
<%= icon(icon, color: active ? "current" : "default", custom: icon_custom) %> <%= icon(icon, color: active ? "current" : "default", custom: icon_custom) %>
<% end %> <% end %>

View file

@ -49,15 +49,15 @@
<div class="shadow-border-xs rounded-lg bg-container min-w-fit"> <div class="shadow-border-xs rounded-lg bg-container min-w-fit">
<% classification_group.account_groups.each do |account_group| %> <% classification_group.account_groups.each do |account_group| %>
<details class="group rounded-lg open:bg-surface font-medium text-sm"> <details class="group rounded-lg open:bg-surface font-medium text-sm">
<summary class="cursor-pointer p-4 group-open:bg-surface bg-container rounded-lg flex items-center justify-between"> <summary class="focus-visible:outline-none focus-visible:ring-0 cursor-pointer p-4 group-open:bg-surface bg-container rounded-lg flex items-center justify-between">
<div class="w-40 shrink-0 flex items-center gap-4"> <div class="w-40 shrink-0 flex items-center gap-4">
<%= icon("chevron-right", class: "group-open:rotate-90") %> <%= icon("chevron-right", class: "group-open:rotate-90") %>
<p><%= account_group.name %></p> <p><%= account_group.name %></p>
</div> </div>
<div class="ml-auto flex items-center text-right gap-6"> <div class="flex items-center justify-between text-right gap-6">
<div class="w-24 shrink-0 flex items-center justify-end gap-2"> <div class="w-28 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %> <%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %>
</div> </div>
@ -77,7 +77,7 @@
</div> </div>
<div class="ml-auto flex items-center text-right gap-6"> <div class="ml-auto flex items-center text-right gap-6">
<div class="w-24 shrink-0 flex items-center justify-end gap-2"> <div class="w-28 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %> <%= render "pages/dashboard/group_weight", weight: account.weight, color: account_group.color %>
</div> </div>

View file

@ -1,9 +1,9 @@
<%# locals: (weight:, color:) %> <%# locals: (weight:, color:) %>
<div class="flex items-center gap-2"> <div class="w-full flex items-center justify-between gap-2">
<div class="flex gap-[3px]"> <div class="flex gap-[3px]">
<% 10.times do |i| %> <% 10.times do |i| %>
<div class="w-[2px] h-[10px] rounded-lg <%= i < (weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= color %>;"></div> <div class="w-0.5 h-2.5 rounded-lg <%= i < (weight / 10.0).ceil ? "" : "opacity-20" %>" style="background-color: <%= color %>;"></div>
<% end %> <% end %>
</div> </div>
<p class="text-sm"><%= number_to_percentage(weight, precision: 2) %></p> <p class="text-sm"><%= number_to_percentage(weight, precision: 2) %></p>

View file

@ -1,5 +1,5 @@
<%# locals: (totals:) %> <%# locals: (totals:) %>
<div class="grid grid-cols-1 md:grid-cols-3 bg-container rounded-xl shadow-border-xs md:divide-x divide-y md:divide-y-0 divide-alpha-black-100"> <div class="grid grid-cols-1 md:grid-cols-3 bg-container rounded-xl shadow-border-xs md:divide-x divide-y md:divide-y-0 divide-alpha-black-100 theme-dark:divide-alpha-white-200">
<div class="p-4 space-y-2"> <div class="p-4 space-y-2">
<p class="text-sm text-secondary">Total transactions</p> <p class="text-sm text-secondary">Total transactions</p>
<p class="text-primary font-medium text-xl" id="total-transactions"><%= totals.transactions_count.round(0) %></p> <p class="text-primary font-medium text-xl" id="total-transactions"><%= totals.transactions_count.round(0) %></p>

View file

@ -3,10 +3,10 @@
<div id="<%= dom_id(transaction, "transfer_match") %>" class="flex items-center gap-1"> <div id="<%= dom_id(transaction, "transfer_match") %>" class="flex items-center gap-1">
<% if transaction.transfer.confirmed? %> <% if transaction.transfer.confirmed? %>
<span title="<%= transaction.transfer.payment? ? "Payment" : "Transfer" %> is confirmed"> <span title="<%= transaction.transfer.payment? ? "Payment" : "Transfer" %> is confirmed">
<%= icon "link-2", size: "sm", class: "text-indigo-600" %> <%= icon "link-2", size: "sm", class: "text-secondary" %>
</span> </span>
<% elsif transaction.transfer.pending? %> <% elsif transaction.transfer.pending? %>
<span class="inline-flex items-center rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-medium text-indigo-700"> <span class="inline-flex items-center rounded-full bg-surface-inset px-2 py-0.5 text-xs font-medium text-secondary">
Auto-matched Auto-matched
</span> </span>
@ -14,7 +14,7 @@
method: :patch, method: :patch,
class: "text-secondary hover:text-gray-800 flex items-center justify-center cursor-pointer", class: "text-secondary hover:text-gray-800 flex items-center justify-center cursor-pointer",
title: "Confirm match" do %> title: "Confirm match" do %>
<%= icon "check", size: "sm", class: "text-indigo-400 hover:text-indigo-600" %> <%= icon "check", size: "sm", class: "text-secondary hover:text-primary" %>
<% end %> <% end %>
<%= button_to transfer_path(transaction.transfer, transfer: { status: "rejected" }), <%= button_to transfer_path(transaction.transfer, transfer: { status: "rejected" }),
@ -22,7 +22,7 @@
data: { turbo: false }, data: { turbo: false },
class: "text-secondary hover:text-gray-800 flex items-center justify-center cursor-pointer", class: "text-secondary hover:text-gray-800 flex items-center justify-center cursor-pointer",
title: "Reject match" do %> title: "Reject match" do %>
<%= icon "x", size: "sm", class: "text-subdued hover:text-gray-600" %> <%= icon "x", size: "sm", class: "text-subdued hover:text-primary" %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>