diff --git a/app/assets/tailwind/allocation_slider.css b/app/assets/tailwind/allocation_slider.css
new file mode 100644
index 0000000..23278b1
--- /dev/null
+++ b/app/assets/tailwind/allocation_slider.css
@@ -0,0 +1,55 @@
+@layer components {
+ .allocation-slider {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 0.5rem;
+ border-radius: 9999px;
+ background: transparent;
+ cursor: pointer;
+ }
+
+ .allocation-slider::-webkit-slider-runnable-track {
+ height: 0.5rem;
+ border-radius: 9999px;
+ background: linear-gradient(
+ to right,
+ var(--slider-color) 0%,
+ var(--slider-color) var(--slider-value, 0%),
+ var(--color-line-soft) var(--slider-value, 0%),
+ var(--color-line-soft) 100%
+ );
+ }
+
+ .allocation-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 1rem;
+ width: 1rem;
+ margin-top: -0.25rem;
+ border-radius: 9999px;
+ background: var(--slider-color);
+ border: 2px solid var(--color-surface);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
+ }
+
+ .allocation-slider::-moz-range-track {
+ height: 0.5rem;
+ border-radius: 9999px;
+ background: var(--color-line-soft);
+ }
+
+ .allocation-slider::-moz-range-progress {
+ height: 0.5rem;
+ border-radius: 9999px;
+ background: var(--slider-color);
+ }
+
+ .allocation-slider::-moz-range-thumb {
+ height: 1rem;
+ width: 1rem;
+ border: 2px solid var(--color-surface);
+ border-radius: 9999px;
+ background: var(--slider-color);
+ }
+}
diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css
index b01976e..73a839d 100644
--- a/app/assets/tailwind/application.css
+++ b/app/assets/tailwind/application.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@import "./allocation_slider.css";
@theme {
--color-canvas: #F5F2ED;
diff --git a/app/javascript/controllers/allocation_slider_controller.js b/app/javascript/controllers/allocation_slider_controller.js
new file mode 100644
index 0000000..550c218
--- /dev/null
+++ b/app/javascript/controllers/allocation_slider_controller.js
@@ -0,0 +1,32 @@
+import { Controller } from "@hotwired/stimulus"
+
+const currency = new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: 0,
+})
+
+export default class extends Controller {
+ static targets = ["input", "percent", "dollar", "perpetuity"]
+ static values = {
+ ongoingAmount: Number,
+ payoutRate: Number,
+ }
+
+ update() {
+ const percent = Number(this.inputTarget.value)
+ const dollar = Math.round((percent / 100) * this.ongoingAmountValue)
+ const perpetuity = Math.round(dollar * this.payoutRateValue)
+
+ this.inputTarget.style.setProperty("--slider-value", `${percent}%`)
+ this.percentTargets.forEach((el) => (el.textContent = `${percent}%`))
+ this.dollarTargets.forEach((el) => (el.textContent = currency.format(dollar)))
+ if (this.hasPerpetuityTarget) {
+ this.perpetuityTarget.textContent = currency.format(perpetuity)
+ }
+ }
+
+ save() {
+ this.inputTarget.form?.requestSubmit()
+ }
+}
diff --git a/app/javascript/controllers/range_controller.js b/app/javascript/controllers/range_controller.js
deleted file mode 100644
index ea40c92..0000000
--- a/app/javascript/controllers/range_controller.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Controller } from "@hotwired/stimulus"
-
-// Mirrors a range input's value into an output element as it moves.
-export default class extends Controller {
- static targets = ["input", "output"]
-
- update() {
- this.outputTarget.textContent = this.inputTarget.value
- }
-}
diff --git a/app/views/scenarios/_allocation.html.erb b/app/views/scenarios/_allocation.html.erb
index 6670993..048f6ec 100644
--- a/app/views/scenarios/_allocation.html.erb
+++ b/app/views/scenarios/_allocation.html.erb
@@ -1,25 +1,86 @@
-<%# locals: (allocation:, scenario:) %>
-
-
-
-
<%= allocation.display_label %>
- <% if allocation.note.present? %>
-
<%= allocation.note %>
- <% end %>
-
-
-
- <%= allocation.ongoing? ? "#{allocation.percentage}%" : number_to_currency(allocation.amount, precision: 0) %>
-
-
- <%= button_to "✕", scenario_allocation_path(allocation.scenario, allocation), method: :delete,
- class: "text-ink-faint hover:text-danger cursor-pointer bg-transparent p-0 leading-none",
- data: { turbo_confirm: "Remove this allocation?" } %>
+<%# locals: (allocation:, scenario:, allocation_counter: 0) %>
+<% if allocation.ongoing? %>
+ <% color = allocation_chart_color(allocation_counter) %>
+
+
+
+
+
<%= allocation.display_label %>
+
+ <%= allocation.note.presence || "No additional preferences" %>
+
+
+
+
+ <%= allocation.percentage %>%
+ <%= number_to_currency(allocation.dollar_amount, precision: 0) %>
+
+
+ <%= button_to scenario_allocation_path(allocation.scenario, allocation), method: :delete,
+ class: "text-ink-faint hover:text-danger cursor-pointer bg-transparent p-0 leading-none",
+ form_class: "leading-none", aria: { label: "Remove allocation" },
+ data: { turbo_confirm: "Remove this allocation?" } do %>
+
+ <% end %>
+
+
+
+
+ <%= form_with url: scenario_allocation_path(scenario, allocation), method: :patch, class: "flex-1 min-w-0 leading-none" do %>
+ <%= range_field_tag "allocation[percentage]", allocation.percentage, min: 0, max: 100, step: 1,
+ style: "--slider-color: #{color}; --slider-value: #{allocation.percentage}%",
+ data: { allocation_slider_target: "input", action: "input->allocation-slider#update change->allocation-slider#save" },
+ class: "allocation-slider block" %>
+ <% end %>
+
+ <%= allocation.percentage %>%
+ <%= number_to_currency(allocation.dollar_amount, precision: 0) %>
+
+
+
+
+ ∞
+ Est. <%= number_to_currency(allocation.perpetuity_annual_amount, precision: 0) %>
+ annually in perpetuity
+ (<%= (Allocation::Ongoing::PERPETUITY_PAYOUT_RATE * 100).to_i %>% of principal)
+
+
+ <%= render "allocation_modal", allocation: allocation, scenario: scenario, color: color %>
+<% else %>
+
+
+
+
<%= allocation.display_label %>
+ <% if allocation.note.present? %>
+
<%= allocation.note %>
+ <% end %>
+
+
+
+ <%= number_to_currency(allocation.amount, precision: 0) %>
+
+
+ <%= button_to "✕", scenario_allocation_path(allocation.scenario, allocation), method: :delete,
+ class: "text-ink-faint hover:text-danger cursor-pointer bg-transparent p-0 leading-none",
+ data: { turbo_confirm: "Remove this allocation?" } %>
+
+
- <%= render "allocation_modal", allocation: allocation, scenario: scenario %>
-
+ <%= render "allocation_modal", allocation: allocation, scenario: scenario %>
+
+<% end %>
diff --git a/app/views/scenarios/_allocation_modal.html.erb b/app/views/scenarios/_allocation_modal.html.erb
index 0433ebe..6d0736c 100644
--- a/app/views/scenarios/_allocation_modal.html.erb
+++ b/app/views/scenarios/_allocation_modal.html.erb
@@ -1,8 +1,9 @@
-<%# locals: (allocation:, scenario:) %>
+<%# locals: (allocation:, scenario:, color: nil) %>
<% klass = allocation.class %>
<% editing = allocation.persisted? %>
<% suffix = editing ? dom_id(allocation) : klass.model_name.element %>
<% percentage = allocation.percentage || 20 %>
+<% color ||= ScenariosHelper::CHART_COLORS.first %>
+ <% end %>