diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index f97f63a5..4af14181 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -71,18 +71,22 @@ /* Typography */ .prose { - @apply max-w-none; + @apply max-w-none text-primary; + + a { + @apply text-link; + } h2 { - @apply text-xl font-medium; + @apply text-xl font-medium text-primary; } h3 { - @apply text-lg font-medium; + @apply text-lg font-medium text-primary; } li { - @apply m-0; + @apply m-0 text-primary; } details { diff --git a/app/assets/tailwind/maybe-design-system.css b/app/assets/tailwind/maybe-design-system.css index e03ccb1b..c8a583b8 100644 --- a/app/assets/tailwind/maybe-design-system.css +++ b/app/assets/tailwind/maybe-design-system.css @@ -26,6 +26,11 @@ --color-destructive: var(--color-red-600); --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 */ --color-gray-25: #FAFAFA; --color-gray-50: #F7F7F7; @@ -250,6 +255,10 @@ --color-destructive: var(--color-red-400); --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-sm: 0px 1px 6px 0px --alpha(var(--color-white) / 8%); --shadow-md: 0px 4px 8px -2px --alpha(var(--color-white) / 8%); diff --git a/app/javascript/controllers/category_controller.js b/app/javascript/controllers/category_controller.js index dfbcd029..fdd47729 100644 --- a/app/javascript/controllers/category_controller.js +++ b/app/javascript/controllers/category_controller.js @@ -1,22 +1,36 @@ -import { Controller } from "@hotwired/stimulus" -import Pickr from '@simonwep/pickr' +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 targets = [ + "pickerBtn", + "colorInput", + "colorsSection", + "paletteSection", + "pickerSection", + "colorPreview", + "avatar", + "details", + "icon", + "validationMessage", + "selection", + "colorPickerRadioBtn", + ]; + static values = { presetColors: Array, }; initialize() { - this.pickerBtnTarget.addEventListener('click', () => { + this.pickerBtnTarget.addEventListener("click", () => { this.showPaletteSection(); }); - this.colorInputTarget.addEventListener('input', (e) => { + this.colorInputTarget.addEventListener("input", (e) => { this.picker.setColor(e.target.value); }); - this.detailsTarget.addEventListener('toggle', (e) => { + this.detailsTarget.addEventListener("toggle", (e) => { if (!this.colorInputTarget.checkValidity()) { e.preventDefault(); this.colorInputTarget.reportValidity(); @@ -38,7 +52,7 @@ export default class extends Controller { this.picker = Pickr.create({ el: this.pickerBtnTarget, - theme: 'monolith', + theme: "monolith", container: ".pickerContainer", useAsButton: 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 rgbacolor = color.toRGBA(); @@ -74,23 +88,26 @@ export default class extends Controller { handleIconColorChange(e) { const selectedIcon = e.target; this.selectedIcon = selectedIcon; - + const currentColor = this.colorInputTarget.value; - - this.iconTargets.forEach(icon => { + + this.iconTargets.forEach((icon) => { const iconWrapper = icon.nextElementSibling; - iconWrapper.style.removeProperty("background-color") - iconWrapper.style.color = "black"; + iconWrapper.style.removeProperty("background-color"); + iconWrapper.style.removeProperty("color"); }); 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") + 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); } @@ -112,7 +129,9 @@ export default class extends Controller { handleContrastValidation(contrastRatio) { 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"); } else { @@ -121,7 +140,7 @@ export default class extends Controller { } } - autoAdjust(e){ + autoAdjust(e) { const currentRGBA = this.picker.getColor(); const adjustedRGBA = this.darkenColor(currentRGBA).toString(); this.picker.setColor(adjustedRGBA); @@ -129,22 +148,29 @@ export default class extends Controller { handleParentChange(e) { 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; } - 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))); + 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 => { + luminance([r, g, b]) { + const toLinear = (c) => { const scaled = c / 255; - return scaled <= 0.04045 - ? scaled / 12.92 + 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); @@ -162,12 +188,15 @@ export default class extends Controller { 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)) { + 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] + darkened[3], ]; contrastRatio = this.contrast(darkened, backgroundColor); } @@ -177,23 +206,23 @@ export default class extends Controller { showPaletteSection() { this.initPicker(); - this.colorsSectionTarget.classList.add('hidden'); - this.paletteSectionTarget.classList.remove('hidden'); - this.pickerSectionTarget.classList.remove('hidden'); + 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'); + 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')) { + if (this.colorsSectionTarget.classList.contains("hidden")) { this.showColorsSection(); } else { this.showPaletteSection(); diff --git a/app/javascript/controllers/donut_chart_controller.js b/app/javascript/controllers/donut_chart_controller.js index 55c7cbb3..0c52104b 100644 --- a/app/javascript/controllers/donut_chart_controller.js +++ b/app/javascript/controllers/donut_chart_controller.js @@ -136,13 +136,13 @@ export default class extends Controller { .attr("fill", function () { if (this.dataset.segmentId === segmentId) { if (this.dataset.segmentId === unusedSegmentId) { - return "#A3A3A3"; + return "var(--budget-unused-fill)"; } return this.dataset.originalColor; } - return "#F0F0F0"; + return "var(--budget-unallocated-fill)"; }); this.defaultContentTarget.classList.add("hidden"); diff --git a/app/javascript/controllers/sidebar_tabs_controller.js b/app/javascript/controllers/sidebar_tabs_controller.js new file mode 100644 index 00000000..f88a70f7 --- /dev/null +++ b/app/javascript/controllers/sidebar_tabs_controller.js @@ -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"); + } + }); + } +} diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js index 4a7a9f48..10aa7b9a 100644 --- a/app/javascript/controllers/theme_controller.js +++ b/app/javascript/controllers/theme_controller.js @@ -4,7 +4,6 @@ export default class extends Controller { static values = { userPreference: String }; connect() { - this.applyTheme(); this.startSystemThemeListener(); } @@ -45,7 +44,7 @@ export default class extends Controller { if (isDark) { document.documentElement.setAttribute("data-theme", "dark"); } 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() { const currentTheme = document.documentElement.getAttribute("data-theme"); if (currentTheme === "dark") { - this.toLight(); + this.setTheme(false); } else { - this.toDark(); + this.setTheme(true); } } diff --git a/app/javascript/controllers/time_series_chart_controller.js b/app/javascript/controllers/time_series_chart_controller.js index dc60e8cc..3de33c57 100644 --- a/app/javascript/controllers/time_series_chart_controller.js +++ b/app/javascript/controllers/time_series_chart_controller.js @@ -138,36 +138,48 @@ export default class extends Controller { .attr("x1", this._d3XScale.range()[0]) .attr("x2", this._d3XScale.range()[1]); + // First stop - solid trend color gradient .append("stop") .attr("class", "start-color") .attr("offset", "0%") .attr("stop-color", this.dataValue.trend.color); + // Second stop - trend color right before split gradient .append("stop") - .attr("class", "middle-color") + .attr("class", "split-before") .attr("offset", "100%") .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 .append("stop") .attr("class", "end-color") .attr("offset", "100%") - .attr("class", "fg-subdued") - .attr("stop-color", "currentColor"); + .attr("stop-color", "var(--color-gray-400)"); } _setTrendlineSplitAt(percent) { + const position = percent * 100; + + // Update both stops at the split point this._d3Svg .select(`#${this.element.id}-split-gradient`) - .select(".middle-color") - .attr("offset", `${percent * 100}%`); + .select(".split-before") + .attr("offset", `${position}%`); this._d3Svg .select(`#${this.element.id}-split-gradient`) - .select(".end-color") - .attr("offset", `${percent * 100}%`); + .select(".split-after") + .attr("offset", `${position}%`); this._d3Svg .select(`#${this.element.id}-trendline-gradient-rect`) diff --git a/app/models/budget.rb b/app/models/budget.rb index c2393da4..e53b6f09 100644 --- a/app/models/budget.rb +++ b/app/models/budget.rb @@ -131,14 +131,14 @@ class Budget < ApplicationRecord unused_segment_id = "unused" # 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| { color: bc.category.color, amount: budget_category_actual_spending(bc), id: bc.id } end 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 segments diff --git a/app/models/budget_category.rb b/app/models/budget_category.rb index e909a7e8..a2a84850 100644 --- a/app/models/budget_category.rb +++ b/app/models/budget_category.rb @@ -79,14 +79,14 @@ class BudgetCategory < ApplicationRecord unused_segment_id = "unused" 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 } ] 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 - 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 segments diff --git a/app/views/accounts/_account_sidebar_tabs.html.erb b/app/views/accounts/_account_sidebar_tabs.html.erb index 1ed98300..9ca2781c 100644 --- a/app/views/accounts/_account_sidebar_tabs.html.erb +++ b/app/views/accounts/_account_sidebar_tabs.html.erb @@ -21,16 +21,17 @@ <% end %> - <%= 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| %> - <% nav.with_btn(id: "assets", label: "Assets") %> - <% nav.with_btn(id: "debts", label: "Debts") %> - <% nav.with_btn(id: "all", label: "All") %> - <% end %> +
<%= format_money(budget.actual_spending_money) %> spent
<% if budget.available_to_spend.negative? %> - <%= format_money(budget.available_to_spend_money.abs) %> over + <%= format_money(budget.available_to_spend_money.abs) %> over <% else %> <%= format_money(budget.available_to_spend_money) %> left <% end %> diff --git a/app/views/budgets/show.html.erb b/app/views/budgets/show.html.erb index 5d92a2f2..ff19bab1 100644 --- a/app/views/budgets/show.html.erb +++ b/app/views/budgets/show.html.erb @@ -16,31 +16,26 @@