diff --git a/app/components/DS/dialog.rb b/app/components/DS/dialog.rb
index 11fce8f02..a8dba3d0d 100644
--- a/app/components/DS/dialog.rb
+++ b/app/components/DS/dialog.rb
@@ -33,7 +33,7 @@ class DS::Dialog < DesignSystemComponent
end
end
- attr_reader :variant, :auto_open, :reload_on_close, :width, :disable_frame, :disable_click_outside, :opts
+ attr_reader :variant, :auto_open, :reload_on_close, :width, :disable_frame, :content_class, :disable_click_outside, :opts
VARIANTS = %w[modal drawer].freeze
WIDTHS = {
@@ -43,13 +43,14 @@ class DS::Dialog < DesignSystemComponent
full: "lg:max-w-full"
}.freeze
- def initialize(variant: "modal", auto_open: true, reload_on_close: false, width: "md", frame: nil, disable_frame: false, disable_click_outside: false, **opts)
+ def initialize(variant: "modal", auto_open: true, reload_on_close: false, width: "md", frame: nil, disable_frame: false, content_class: nil, disable_click_outside: false, **opts)
@variant = variant.to_sym
@auto_open = auto_open
@reload_on_close = reload_on_close
@width = width.to_sym
@frame = frame
@disable_frame = disable_frame
+ @content_class = content_class
@disable_click_outside = disable_click_outside
@opts = opts
end
@@ -92,7 +93,8 @@ class DS::Dialog < DesignSystemComponent
class_names(
"flex flex-col bg-container rounded-xl shadow-border-xs mx-3 lg:mx-0 w-full overflow-hidden",
- variant_classes
+ variant_classes,
+ content_class
)
end
@@ -104,7 +106,7 @@ class DS::Dialog < DesignSystemComponent
data[:DS__dialog_auto_open_value] = auto_open
data[:DS__dialog_reload_on_close_value] = reload_on_close
data[:DS__dialog_disable_click_outside_value] = disable_click_outside
- data[:action] = [ "mousedown->DS--dialog#clickOutside", data[:action] ].compact.join(" ")
+ data[:action] = [ "click->DS--dialog#clickOutside", data[:action] ].compact.join(" ")
data[:hotkey] = "esc:DS--dialog#close"
merged_opts[:data] = data
diff --git a/app/javascript/controllers/cashflow_expand_controller.js b/app/javascript/controllers/cashflow_expand_controller.js
new file mode 100644
index 000000000..a14a3608e
--- /dev/null
+++ b/app/javascript/controllers/cashflow_expand_controller.js
@@ -0,0 +1,23 @@
+import { Controller } from "@hotwired/stimulus";
+
+export default class extends Controller {
+ open() {
+ const dialog = this.element.querySelector("dialog");
+ if (!dialog) return;
+
+ if (typeof this.originalDraggable === "undefined") {
+ this.originalDraggable = this.element.getAttribute("draggable");
+ }
+ this.element.setAttribute("draggable", "false");
+
+ dialog.showModal();
+ }
+
+ restore() {
+ if (this.originalDraggable === undefined) return;
+ this.originalDraggable
+ ? this.element.setAttribute("draggable", this.originalDraggable)
+ : this.element.removeAttribute("draggable");
+ this.originalDraggable = undefined;
+ }
+}
diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js
index bb100ab7b..6ce64b470 100644
--- a/app/javascript/controllers/dashboard_sortable_controller.js
+++ b/app/javascript/controllers/dashboard_sortable_controller.js
@@ -77,6 +77,9 @@ export default class extends Controller {
);
if (!section) return;
+ // Respect strict draggable="false" which might be set by other controllers (e.g. expand-controller)
+ if (section.getAttribute("draggable") === "false") return;
+
this.pendingSection = section;
this.touchStartY = event.touches[0].clientY;
this.currentTouchY = this.touchStartY;
diff --git a/app/javascript/controllers/sankey_chart_controller.js b/app/javascript/controllers/sankey_chart_controller.js
index f97714ac9..5a1b9bb44 100644
--- a/app/javascript/controllers/sankey_chart_controller.js
+++ b/app/javascript/controllers/sankey_chart_controller.js
@@ -45,6 +45,9 @@ export default class extends Controller {
const { nodes = [], links = [] } = this.dataValue || {};
if (!nodes.length || !links.length) return;
+ // Hide tooltip and reset any hover states before redrawing
+ this.#hideTooltip();
+
d3.select(this.element).selectAll("svg").remove();
const width = this.element.clientWidth || 600;
@@ -221,7 +224,7 @@ export default class extends Controller {
.style("cursor", "default")
.style("opacity", d => hiddenLabels.has(d.index) ? 0 : 1)
.style("transition", "opacity 0.2s ease")
- .each(function(d) {
+ .each(function (d) {
const textEl = d3.select(this);
textEl.selectAll("tspan").remove();
@@ -241,7 +244,9 @@ export default class extends Controller {
// Calculate which labels should be hidden to prevent overlap
#calculateHiddenLabels(nodes) {
const hiddenLabels = new Set();
- const minSpacing = this.constructor.MIN_LABEL_SPACING;
+ const height = this.element.clientHeight || 400;
+ const isLargeGraph = height > 600;
+ const minSpacing = isLargeGraph ? this.constructor.MIN_LABEL_SPACING * 0.7 : this.constructor.MIN_LABEL_SPACING;
// Group nodes by column (using depth which d3-sankey assigns)
const columns = new Map();
@@ -260,8 +265,11 @@ export default class extends Controller {
columnNodes.forEach(node => {
const nodeY = (node.y0 + node.y1) / 2;
+ const nodeHeight = node.y1 - node.y0;
- if (nodeY - lastVisibleY < minSpacing) {
+ if (isLargeGraph && nodeHeight > minSpacing * 1.5) {
+ lastVisibleY = nodeY;
+ } else if (nodeY - lastVisibleY < minSpacing) {
// Too close to previous visible label, hide this one
hiddenLabels.add(node.index);
} else {
@@ -338,7 +346,8 @@ export default class extends Controller {
// Tooltip methods
#createTooltip() {
- this.tooltip = d3.select("body")
+ const dialog = this.element.closest("dialog");
+ this.tooltip = d3.select(dialog || document.body)
.append("div")
.attr("class", "bg-gray-700 text-white text-sm p-2 rounded pointer-events-none absolute z-50")
.style("opacity", 0)
@@ -348,32 +357,44 @@ export default class extends Controller {
#showTooltip(event, value, percentage, title = null) {
if (!this.tooltip) this.#createTooltip();
- const formattedValue = this.#formatCurrency(value);
- const percentageText = percentage ? `${percentage}%` : "0%";
const content = title
- ? `${title}
${formattedValue} (${percentageText})`
- : `${formattedValue} (${percentageText})`;
+ ? `${title}
${this.#formatCurrency(value)} (${percentage || 0}%)`
+ : `${this.#formatCurrency(value)} (${percentage || 0}%)`;
+
+ const isInDialog = !!this.element.closest("dialog");
+ const x = isInDialog ? event.clientX : event.pageX;
+ const y = isInDialog ? event.clientY : event.pageY;
this.tooltip
.html(content)
- .style("left", `${event.pageX + 10}px`)
- .style("top", `${event.pageY - 10}px`)
+ .style("position", isInDialog ? "fixed" : "absolute")
+ .style("left", `${x + 10}px`)
+ .style("top", `${y - 10}px`)
.transition()
.duration(100)
.style("opacity", 1);
}
#updateTooltipPosition(event) {
- this.tooltip
- ?.style("left", `${event.pageX + 10}px`)
- .style("top", `${event.pageY - 10}px`);
+ if (this.tooltip) {
+ const isInDialog = !!this.element.closest("dialog");
+ const x = isInDialog ? event.clientX : event.pageX;
+ const y = isInDialog ? event.clientY : event.pageY;
+
+ this.tooltip
+ ?.style("left", `${x + 10}px`)
+ .style("top", `${y - 10}px`);
+ }
}
#hideTooltip() {
- this.tooltip
- ?.transition()
- .duration(100)
- .style("opacity", 0);
+ if (this.tooltip) {
+ this.tooltip
+ ?.transition()
+ .duration(100)
+ .style("opacity", 0)
+ .style("pointer-events", "none");
+ }
}
#formatCurrency(value) {
diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb
index 3b51717e8..7ac96bc80 100644
--- a/app/views/pages/dashboard.html.erb
+++ b/app/views/pages/dashboard.html.erb
@@ -35,7 +35,7 @@
class="bg-container rounded-xl shadow-border-xs transition-all group focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-2"
data-dashboard-sortable-target="section"
data-section-key="<%= section[:key] %>"
- data-controller="dashboard-section"
+ data-controller="dashboard-section<%= ' cashflow-expand' if section[:key] == 'cashflow_sankey' %>"
data-dashboard-section-section-key-value="<%= section[:key] %>"
data-dashboard-section-collapsed-value="<%= Current.user.dashboard_section_collapsed?(section[:key]) %>"
draggable="true"
@@ -65,12 +65,23 @@
<%= t(section[:title]) %>
-
+