From 4040b6efacc4de84c4ed1fdb4ee35aaf78923681 Mon Sep 17 00:00:00 2001 From: Mike Dalton Date: Sun, 28 Jun 2026 21:21:21 -0500 Subject: [PATCH 1/2] Collapse additional preferences to two rows with a show more/less link Each category group of additional-preference chips now clips to two rows by default, with an inline "Show more (+N)" / "Show less" link at the end of the last visible row. A new chip-overflow Stimulus controller measures chip positions to decide what to hide and recomputes on resize and when the dialog opens. Groups that already fit in two rows show no link, and a group defaults to expanded when one of its chips is already selected. Co-Authored-By: Claude Opus 4.8 --- .../controllers/chip_overflow_controller.js | 74 +++++++++++++++++++ .../_additional_preferences.html.erb | 6 +- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 app/javascript/controllers/chip_overflow_controller.js diff --git a/app/javascript/controllers/chip_overflow_controller.js b/app/javascript/controllers/chip_overflow_controller.js new file mode 100644 index 0000000..cf687a7 --- /dev/null +++ b/app/javascript/controllers/chip_overflow_controller.js @@ -0,0 +1,74 @@ +import { Controller } from "@hotwired/stimulus" + +// Collapses a wrapping row of chips down to a fixed number of rows, with a +// toggle button at the end of the last visible row to reveal the rest. +export default class extends Controller { + static targets = ["chip", "button"] + static values = { rows: { type: Number, default: 2 }, expanded: Boolean } + + connect() { + // Default to expanded when a chip in this category is already selected, so + // the active preference is visible without having to click "Show more". + if (this.chipTargets.some((chip) => chip.querySelector("input")?.checked)) { + this.expandedValue = true + } + this.onResize = () => this.layout() + this.resizeObserver = new ResizeObserver(this.onResize) + this.resizeObserver.observe(this.element) + window.addEventListener("resize", this.onResize) + this.layout() + } + + disconnect() { + if (this.resizeObserver) this.resizeObserver.disconnect() + window.removeEventListener("resize", this.onResize) + } + + toggle() { + this.expandedValue = !this.expandedValue + this.layout() + } + + layout() { + if (!this.hasButtonTarget) return + + // Reset: reveal everything so positions can be measured. + this.chipTargets.forEach((chip) => (chip.hidden = false)) + this.buttonTarget.hidden = true + + if (this.expandedValue) { + this.buttonTarget.textContent = "Show less" + this.buttonTarget.hidden = false + return + } + + const tops = [...new Set(this.chipTargets.map((chip) => Math.round(chip.offsetTop)))].sort( + (a, b) => a - b, + ) + + // Already fits within the row budget — nothing to collapse. + if (tops.length <= this.rowsValue) return + + const cutoff = tops[this.rowsValue - 1] + let hidden = 0 + this.chipTargets.forEach((chip) => { + if (Math.round(chip.offsetTop) > cutoff) { + chip.hidden = true + hidden++ + } + }) + + // Reveal the button at the end of the last visible row. If it wrapped past + // the cutoff, hide trailing chips until it fits. + this.buttonTarget.hidden = false + const visibleChips = this.chipTargets.filter((chip) => !chip.hidden) + let i = visibleChips.length - 1 + while (Math.round(this.buttonTarget.offsetTop) > cutoff && i >= 0) { + visibleChips[i].hidden = true + hidden++ + i-- + } + + this.buttonTarget.textContent = `Show more (+${hidden})` + } +} diff --git a/app/views/scenarios/_additional_preferences.html.erb b/app/views/scenarios/_additional_preferences.html.erb index ed452df..dc3b276 100644 --- a/app/views/scenarios/_additional_preferences.html.erb +++ b/app/views/scenarios/_additional_preferences.html.erb @@ -14,14 +14,16 @@ <% roots = roots_by_type[klass_name] %> <% next if roots.blank? %>

<%= klass_name.constantize.tab_label %>

-
+
<% roots.each do |cat| %> -
<% end %>
From 0cd18ffef2c6c25de75e36b9ccbf66c3802ef1c6 Mon Sep 17 00:00:00 2001 From: Mike Dalton Date: Sun, 28 Jun 2026 21:23:35 -0500 Subject: [PATCH 2/2] Bump crass to 1.0.7 to fix bundler-audit advisories Resolves four CSS-parsing DoS advisories (GHSA-6jxj-px6v-747w, GHSA-6wmf-3r64-vcwv, GHSA-8vfg-2r28-hvhj, GHSA-wwpr-jff3-395c) flagged by bundler-audit. crass is a transitive dependency of loofah; the bump stays within its ~> 1.0.2 constraint and touches no other gems. Co-Authored-By: Claude Opus 4.8 --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 25d9ebe..5ee8f43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -133,7 +133,7 @@ GEM logger (~> 1.5) concurrent-ruby (1.3.7) connection_pool (3.0.2) - crass (1.0.6) + crass (1.0.7) date (3.5.1) debug (1.11.1) irb (~> 1.10) @@ -498,7 +498,7 @@ CHECKSUMS childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0 connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a - crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e