You are a constitutional council ranking individual git commits for ownership allocation. Compare these two commits. Decide which contributed more lasting value to the project. Judge substance, not spectacle: - Prefer correct, lasting design and real bugfixes over churn, formatting, renames, or generated noise. - Prefer clarity and necessity over sheer line count. A small precise change can beat a large diffuse one. - Do not favor a side merely because its patch is longer or noisier. - Weight what the change does for the project, not the contributor's name. Return ONLY a JSON object: {"winner": "A" or "B", "ratio": "N:M", "explanation": "..."} The explanation must cite concrete differences in the patches (1-3 sentences). Side A — contributor: tommy-mor Side A — commit message: [df769734] Pin vote controls in a compact bottom HUD on compare pages. Keeps ratio, slider, and actions always visible without repeating item titles beside the slider. Co-authored-by: Cursor Side A — unified diff (full patch): diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs index 1822528e3e3b526059899349ba2f73ae0666f944..dcf05df143825f4f8ba9ee3b3b5f8ab2e69950c7 100644 --- a/server/src/html/mod.rs +++ b/server/src/html/mod.rs @@ -528,5 +528,6 @@ mod tests { assert!(SORTER_UI_JS.contains("Math.max(1, v)")); assert!(SORTER_UI_JS.contains("var divisor = gcd(left, right)")); assert!(SORTER_UI_JS.contains("ratioDisplay.textContent = left + ':' + right")); + assert!(SORTER_UI_JS.contains("--vote-slider-pct")); } } diff --git a/server/src/html/vote.rs b/server/src/html/vote.rs index 7be4be152ea331fb7afb7ce598aed27d23e35cc4..872d23ddf6c18fab016a624de85736fdc2dcde63 100644 --- a/server/src/html/vote.rs +++ b/server/src/html/vote.rs @@ -134,6 +134,36 @@ fn vote_back_nav(parent: &ItemId) -> Markup { } } +fn vote_hud_form( + parent: &ItemId, + left: &ItemId, + right: &ItemId, + rpc_json: &str, + next_pair: Option<&(ItemId, ItemId)>, +) -> Markup { + html! { + div id="vote-hud" class="vote-hud" role="region" aria-label="Vote controls" { + form id="vote-compare-form" method="POST" action="/ui" { + input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json); + input type="hidden" name="ratio_left" id="vote-ratio-left" value="1"; + input type="hidden" name="ratio_right" id="vote-ratio-right" value="1"; + div class="vote-hud-inner" { + div class="vote-ratio-readout" { + span class="muted small" { "ratio " } + strong id="vote-ratio-display" { "1:1" } + } + label class="vote-hud-slider" { + input type="range" id="vote-preference-slider" min="0" max="100" value="50" + aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" + aria-label=(format!("Preference: {} vs {}", left.as_str(), right.as_str())); + } + (vote_compare_actions(parent, next_pair)) + } + } + } + } +} + fn vote_compare_actions(parent: &ItemId, next: Option<&(ItemId, ItemId)>) -> Markup { let next_href = next.map(|(l, r)| vote_compare_href(parent, l, r)); html! { @@ -250,41 +280,28 @@ pub async fn vote_page( ); let body = html! { - div class="scope-theme vote-page-grid" style=(scope_theme_style(&parent)) { - section class="vote-compare-shell" { - h1 { "compare" } - (breadcrumb_path(&parent)) - p class="muted vote-compare-scope" { - "ranking children of " - a href=(item_href(&parent)) { (child_title(&tree, &parent)) } - } - div class="vote-compare-pair" { - (vote_compare_item_card(&tree, &left, "vote-compare-left")) - span class="vote-compare-vs" { "vs" } - (vote_compare_item_card(&tree, &right, "vote-compare-right")) - } - (vote_back_nav(&parent)) - form id="vote-compare-form" method="POST" action="/ui" { - input type="hidden" name=(UI_RPC_FIELD) value=(rpc_json); - input type="hidden" name="ratio_left" id="vote-ratio-left" value="1"; - input type="hidden" name="ratio_right" id="vote-ratio-right" value="1"; - div class="vote-ratio-readout" { - span class="muted small" { "ratio " } - strong id="vote-ratio-display" { "1:1" } + div class="scope-theme vote-page" style=(scope_theme_style(&parent)) { + div class="vote-page-grid" { + section class="vote-compare-shell" { + h1 { "compare" } + (breadcrumb_path(&parent)) + p class="muted vote-compare-scope" { + "ranking children of " + a href=(item_href(&parent)) { (child_title(&tree, &parent)) } } - label class="vote-compare-slider-label" { - span id="vote-slider-left-label" { (child_title(&tree, &left)) } - input type="range" id="vote-preference-slider" min="0" max="100" value="50" - aria-valuemin="0" aria-valuemax="100"; - span id="vote-slider-right-label" { (child_title(&tree, &right)) } + div class="vote-compare-pair" { + (vote_compare_item_card(&tree, &left, "vote-compare-left")) + span class="vote-compare-vs" { "vs" } + (vote_compare_item_card(&tree, &right, "vote-compare-right")) + } + (vote_back_nav(&parent)) + div id="vote-edge-history-region" { + (edge_history) } - (vote_compare_actions(&parent, next_pair.as_ref())) - } - div id="vote-edge-history-region" { - (edge_history) } + (vote_ranking_sidebar(&tree, &parent, &left, &right)) } - (vote_ranking_sidebar(&tree, &parent, &left, &right)) + (vote_hud_form(&parent, &left, &right, &rpc_json, next_pair.as_ref())) } }; diff --git a/server/static/sorter.css b/server/static/sorter.css index 7d88114ff3c3d19011c003cf89e5fc32dd23ee74..50e34484c2b50d2eeb18f8d0ec4f34338363811a 100644 --- a/server/static/sorter.css +++ b/server/static/sorter.css @@ -287,6 +287,11 @@ h1 { } } +.vote-page { + --vote-hud-height: 4.75rem; + padding-bottom: calc(var(--vote-hud-height) + 0.75rem); +} + .vote-page-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 360px); @@ -294,6 +299,50 @@ h1 { align-items: start; } +.vote-hud { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + border-top: 2px solid var(--border); + background: color-mix(in oklch, var(--panel) 92%, var(--bg)); + box-shadow: 0 -0.35rem 1.25rem rgba(0, 0, 0, 0.4); + padding: 0.45rem 0.75rem; + backdrop-filter: blur(8px); +} + +.vote-hud-inner { + max-width: 56rem; + margin: 0 auto; + display: grid; + grid-template-columns: min-content minmax(0, 1fr) auto; + gap: 0.5rem 0.65rem; + align-items: center; +} + +.vote-hud #vote-compare-form { + margin: 0; +} + +.vote-hud .vote-ratio-readout { + margin: 0; + text-align: center; +} + +.vote-hud .vote-compare-actions { + margin-top: 0; + justify-content: flex-end; + gap: 0.5rem; +} + +.vote-hud .btn-primary, +.vote-hud .btn-secondary, +.vote-hud .vote-compare-next { + padding: 0.35rem 0.65rem; + font-size: 0.875rem; +} + .vote-compare-shell { min-width: 0; } @@ -366,14 +415,14 @@ h1 { cursor: default; } -.vote-ratio-readout { - margin: 1rem 0 0.25rem; - text-align: center; -} - -.vote-ratio-readout strong { +.vote-hud .vote-ratio-readout strong { color: var(--accent); font-variant-numeric: tabular-nums; + font-size: 1.1rem; +} + +.vote-hud .vote-ratio-readout .small { + font-size: 0.75rem; } .vote-compare-next { @@ -393,25 +442,69 @@ h1 { text-decoration: underline; } -.vote-compare-slider-label { - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: 0.75rem; - align-items: center; - margin: 1.25rem 0; +.vote-hud-slider { + display: block; + margin: 0; + min-width: 0; } -.vote-compare-slider-label input[type="range"] { +.vote-hud-slider input[type="range"] { + -webkit-appearance: none; + appearance: none; + display: block; width: 100%; - min-width: 160px; + height: 1.5rem; + margin: 0; + background: transparent; + cursor: pointer; +} + +.vote-hud-slider input[type="range"]::-webkit-slider-runnable-track { + height: 0.65rem; + border-radius: 0; + border: 2px solid var(--border); + background: linear-gradient( + to right, + var(--accent) 0%, + var(--accent) var(--vote-slider-pct, 50%), + color-mix(in oklch, var(--muted) 55%, var(--bg)) var(--vote-slider-pct, 50%), + color-mix(in oklch, var(--muted) 55%, var(--bg)) 100% + ); +} + +.vote-hud-slider input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 1.1rem; + height: 1.35rem; + margin-top: -0.42rem; + border: 2px solid var(--fg); + border-radius: 0; + background: var(--accent); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.35); +} + +.vote-hud-slider input[type="range"]::-moz-range-track { + height: 0.65rem; + border-radius: 0; + border: 2px solid var(--border); + background: color-mix(in oklch, var(--muted) 55%, var(--bg)); } -#vote-slider-left-label { - text-align: right; +.vote-hud-slider input[type="range"]::-moz-range-progress { + height: 0.65rem; + border-radius: 0; + background: var(--accent); } -#vote-slider-right-label { - text-align: left; +.vote-hud-slider input[type="range"]::-moz-range-thumb { + width: 1.1rem; + height: 1.35rem; + border: 2px solid var(--fg); + border-radius: 0; + background: var(--accent); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.35); + cursor: pointer; } .vote-edge-history-title { @@ -459,4 +552,24 @@ h1 { position: static; max-height: none; } +} + +@media (max-width: 640px) { + .vote-page { + --vote-hud-height: 7.5rem; + } + + .vote-hud-inner { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto; + gap: 0.35rem; + } + + .vote-hud .vote-compare-actions { + justify-content: center; + } + + .vote-hud { + padding-bottom: max(0.45rem, env(safe-area-inset-bottom)); + } } \ No newline at end of file diff --git a/server/static/sorter_ui.js b/server/static/sorter_ui.js index 4c19a053c0fa62c951e8356d35c028b6f0d0f538..c438851b792ad85f8b6106b7c3fad9ea945275f7 100644 --- a/server/static/sorter_ui.js +++ b/server/static/sorter_ui.js @@ -138,6 +138,8 @@ function update() { var v = parseInt(slider.value, 10); if (!Number.isFinite(v)) v = 50; + slider.style.setProperty('--vote-slider-pct', v + '%'); + slider.setAttribute('aria-valuenow', String(v)); var left = Math.max(1, 100 - v); var right = Math.max(1, v); var divisor = gcd(left, right); diff --git a/server/tests/integration_ui.rs b/server/tests/integration_ui.rs index 9d770cda9990eef86e46721a22221aa8c1f9571c..1929d90ec7a35f53592c622699640d0dfc9f763c 100644 --- a/server/tests/integration_ui.rs +++ b/server/tests/integration_ui.rs @@ -185,8 +185,10 @@ async fn vote_page_renders_live_ranking_sidebar() { assert!(html.contains("scope-theme")); assert!(html.contains("--accent: oklch(")); assert!(html.contains("--bg: oklch(")); + assert!(html.contains("vote-hud")); assert!(html.contains("vote-ratio-display")); assert!(html.contains(">1:1<")); + assert!(!html.contains("vote-slider-left-label")); assert!(html.contains("--rank-bg: oklch(")); assert!(html.contains("--rank-fg: #")); assert!(html.contains("data-rank-item=\"alpha\"")); Side B — contributor: tommy-mor Side B — commit message: [df844c52] Stabilize vote HUD layout and color the slider toward the winning side. Fixed-width ratio and action columns stop track reflow flicker; track fill flips to the right when the right item leads. Co-authored-by: Cursor Side B — unified diff (full patch): diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs index dcf05df143825f4f8ba9ee3b3b5f8ab2e69950c7..588cf62bcea608ed4ede363810f022a67b43b960 100644 --- a/server/src/html/mod.rs +++ b/server/src/html/mod.rs @@ -529,5 +529,6 @@ mod tests { assert!(SORTER_UI_JS.contains("var divisor = gcd(left, right)")); assert!(SORTER_UI_JS.contains("ratioDisplay.textContent = left + ':' + right")); assert!(SORTER_UI_JS.contains("--vote-slider-pct")); + assert!(SORTER_UI_JS.contains("dataset.winner")); } } diff --git a/server/src/html/vote.rs b/server/src/html/vote.rs index 872d23ddf6c18fab016a624de85736fdc2dcde63..d32b206e2fb043466c0ce333d7d215dc1a3e91c9 100644 --- a/server/src/html/vote.rs +++ b/server/src/html/vote.rs @@ -154,6 +154,7 @@ fn vote_hud_form( } label class="vote-hud-slider" { input type="range" id="vote-preference-slider" min="0" max="100" value="50" + data-winner="left" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50" aria-label=(format!("Preference: {} vs {}", left.as_str(), right.as_str())); } diff --git a/server/static/sorter.css b/server/static/sorter.css index 50e34484c2b50d2eeb18f8d0ec4f34338363811a..7b485dd232f60363f4eff0c5a362addf191d05c6 100644 --- a/server/static/sorter.css +++ b/server/static/sorter.css @@ -316,9 +316,12 @@ h1 { max-width: 56rem; margin: 0 auto; display: grid; - grid-template-columns: min-content minmax(0, 1fr) auto; + /* Fixed side columns so ratio text / buttons never resize the slider track. */ + grid-template-columns: var(--vote-ratio-col) minmax(0, 1fr) var(--vote-actions-col); gap: 0.5rem 0.65rem; align-items: center; + --vote-ratio-col: 3.25rem; + --vote-actions-col: 11.5rem; } .vote-hud #vote-compare-form { @@ -327,13 +330,23 @@ h1 { .vote-hud .vote-ratio-readout { margin: 0; + width: var(--vote-ratio-col); + min-width: var(--vote-ratio-col); text-align: center; + white-space: nowrap; +} + +.vote-hud .vote-ratio-readout .small { + display: none; } .vote-hud .vote-compare-actions { margin-top: 0; justify-content: flex-end; gap: 0.5rem; + width: var(--vote-actions-col); + min-width: var(--vote-actions-col); + flex-shrink: 0; } .vote-hud .btn-primary, @@ -416,15 +429,13 @@ h1 { } .vote-hud .vote-ratio-readout strong { + display: inline-block; + min-width: 3ch; color: var(--accent); font-variant-numeric: tabular-nums; font-size: 1.1rem; } -.vote-hud .vote-ratio-readout .small { - font-size: 0.75rem; -} - .vote-compare-next { color: var(--accent); text-decoration: none; @@ -459,7 +470,11 @@ h1 { cursor: pointer; } -.vote-hud-slider input[type="range"]::-webkit-slider-runnable-track { +.vote-hud-slider input[type="range"] { + --vote-track-muted: color-mix(in oklch, var(--muted) 55%, var(--bg)); +} + +.vote-hud-slider input[type="range"][data-winner="left"]::-webkit-slider-runnable-track { height: 0.65rem; border-radius: 0; border: 2px solid var(--border); @@ -467,8 +482,21 @@ h1 { to right, var(--accent) 0%, var(--accent) var(--vote-slider-pct, 50%), - color-mix(in oklch, var(--muted) 55%, var(--bg)) var(--vote-slider-pct, 50%), - color-mix(in oklch, var(--muted) 55%, var(--bg)) 100% + var(--vote-track-muted) var(--vote-slider-pct, 50%), + var(--vote-track-muted) 100% + ); +} + +.vote-hud-slider input[type="range"][data-winner="right"]::-webkit-slider-runnable-track { + height: 0.65rem; + border-radius: 0; + border: 2px solid var(--border); + background: linear-gradient( + to right, + var(--vote-track-muted) 0%, + var(--vote-track-muted) var(--vote-slider-pct, 50%), + var(--accent) var(--vote-slider-pct, 50%), + var(--accent) 100% ); } @@ -484,17 +512,35 @@ h1 { box-shadow: 0 2px 0 rgba(0, 0, 0, 0.35); } -.vote-hud-slider input[type="range"]::-moz-range-track { +.vote-hud-slider input[type="range"][data-winner="left"]::-moz-range-track, +.vote-hud-slider input[type="range"][data-winner="right"]::-moz-range-track { height: 0.65rem; border-radius: 0; border: 2px solid var(--border); - background: color-mix(in oklch, var(--muted) 55%, var(--bg)); +} + +.vote-hud-slider input[type="range"][data-winner="left"]::-moz-range-track { + background: linear-gradient( + to right, + var(--accent) 0%, + var(--accent) var(--vote-slider-pct, 50%), + var(--vote-track-muted) var(--vote-slider-pct, 50%), + var(--vote-track-muted) 100% + ); +} + +.vote-hud-slider input[type="range"][data-winner="right"]::-moz-range-track { + background: linear-gradient( + to right, + var(--vote-track-muted) 0%, + var(--vote-track-muted) var(--vote-slider-pct, 50%), + var(--accent) var(--vote-slider-pct, 50%), + var(--accent) 100% + ); } .vote-hud-slider input[type="range"]::-moz-range-progress { - height: 0.65rem; - border-radius: 0; - background: var(--accent); + background: transparent; } .vote-hud-slider input[type="range"]::-moz-range-thumb { @@ -563,6 +609,14 @@ h1 { grid-template-columns: 1fr; grid-template-rows: auto auto auto; gap: 0.35rem; + --vote-ratio-col: 100%; + --vote-actions-col: 100%; + } + + .vote-hud .vote-ratio-readout, + .vote-hud .vote-compare-actions { + width: auto; + min-width: 0; } .vote-hud .vote-compare-actions { diff --git a/server/static/sorter_ui.js b/server/static/sorter_ui.js index c438851b792ad85f8b6106b7c3fad9ea945275f7..f665b45f4fd4771427dc4c37e563e4976873beee 100644 --- a/server/static/sorter_ui.js +++ b/server/static/sorter_ui.js @@ -148,6 +148,7 @@ if (leftInput) leftInput.value = String(left); if (rightInput) rightInput.value = String(right); if (ratioDisplay) ratioDisplay.textContent = left + ':' + right; + slider.dataset.winner = left >= right ? 'left' : 'right'; } slider.addEventListener('input', update); update();