Lift cards off the page and add the first axis from the list
The light page sat a hair under the card, so lists barely showed. The
page takes the fill step instead, as pro-tacts does, and the hovers and
tags that filled with that step move one down so they still show.
Gloss' prose spacing between list items no longer applies inside a
card, where it left a strip of each hovered row unfilled.
A list with no axes now offers a field for its first one in place of
pointing at settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTqTY5gmGmtR4d3kBWGZTX
diff --git a/lib/rank_king/views/list/show.rb b/lib/rank_king/views/list/show.rb
index 6485532..30cb01d 100644
--- a/lib/rank_king/views/list/show.rb
+++ b/lib/rank_king/views/list/show.rb
@@ -48,8 +48,21 @@ module RankKing
dialog(id: "sync-dialog") { render source.form(csrf: @csrf) }
end
+ # Nothing to compare on yet, so the first axis is made here.
def render_items
- p { "No axes yet. Add one in settings to start comparing." }
+ action = "/lists/#{@list.id}/axes"
+ form(method: "post", action: action) do
+ input(type: "hidden", name: "_csrf", value: @csrf.call(action))
+ label(class: "field") do
+ plain "First axis"
+ input(type: "text", name: "name", required: true, placeholder: "Taste")
+ small { "What to rank these items on." }
+ end
+ footer do
+ button(type: "submit", data_variant: "primary") { "Add axis" }
+ end
+ end
+
ul(class: "card") do
@list.items.each { |item| li { item.name } }
end
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 82cdc63..d8997e7 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -113,6 +113,14 @@ module RankKing
r.redirect "/"
end
+ r.post "axes" do
+ name = r.params["name"].to_s.strip
+ r.halt 422 if name.empty?
+
+ axis = Axis.create(list_id: list.id, name: name)
+ r.redirect "/lists/#{list.id}?axis=#{axis.id}"
+ end
+
r.get "axes", Integer, "compare" do |axis_id|
axis = list.axes_dataset.where(id: axis_id).first
r.halt 404 unless axis
diff --git a/public/css/app.css b/public/css/app.css
index 3899300..f7be1aa 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -8,6 +8,38 @@
--rk-icon-more: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E");
}
+/* -------------------- */
+/* Surfaces */
+/* -------------------- */
+
+/* Light mode's page sits a hair under the card (#faf9f7 under #fdfdfb),
+ too fine a step for a card to read against. The page takes the fill
+ step instead, as pro-tacts does: one alias repointed to a value
+ already on the ramp. Dark keeps its own block in tokens.css, so this
+ takes that block's scoping inverted. */
+@media (prefers-color-scheme: light) {
+ :root:not([data-theme="dark"]) {
+ --gl-color-surface-page: var(--gl-color-surface-fill);
+ }
+}
+:root[data-theme="light"] {
+ --gl-color-surface-page: var(--gl-color-surface-fill);
+}
+
+/* What Gloss fills with surface-fill would now match the page and
+ vanish: hovers and tags take the next step down instead. */
+.card > li > a:only-child:hover,
+.card > li > button:only-child:hover,
+.icon-button:hover:not(:disabled),
+:is(button:not(.icon-button), a.btn):not([role="tab"]):not([data-variant="primary"]):not([data-variant="dark"]):not([data-variant="danger"]):hover:not(:disabled):not([aria-disabled="true"]),
+.tag:not([data-dashed]) {
+ background: var(--gl-color-surface-fill-2);
+}
+
+/* Gloss' prose spacing between list items would sit outside each row,
+ leaving a strip of every hovered row unfilled. Rows in a card meet. */
+.card > li + li { margin-top: 0; }
+
/* -------------------- */
/* Page */
/* -------------------- */
diff --git a/test/test_web.rb b/test/test_web.rb
index 71bfb6e..f753fda 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -249,10 +249,35 @@ class TestWeb < Minitest::Test
assert_match(/id="axis-#{value.id}-tab"[^>]*aria-selected="true"/, last_response.body)
end
- def test_show_list_without_axes
+ def test_show_list_without_axes_offers_the_first
create_list(name: "Simple", items: "A\nB")
- get "/lists/#{RankKing::List.first.id}"
- assert_includes last_response.body, "No axes yet."
+ list = RankKing::List.first
+
+ get "/lists/#{list.id}"
+ assert_includes last_response.body, %(action="/lists/#{list.id}/axes")
+ assert_includes last_response.body, "First axis"
+ end
+
+ def test_add_axis_from_the_list
+ create_list(name: "Simple", items: "A\nB")
+ list = RankKing::List.first
+
+ csrf_post "/lists/#{list.id}/axes", { name: " Taste " }, from: "/lists/#{list.id}"
+ axis = list.axes_dataset.first
+ assert_equal "Taste", axis.name
+ assert_equal "/lists/#{list.id}", URI(last_response.location).path
+ assert_equal "axis=#{axis.id}", URI(last_response.location).query
+
+ follow_redirect!
+ assert_includes last_response.body, "Compare on Taste"
+ end
+
+ def test_add_axis_rejects_empty_name
+ create_list(name: "Simple", items: "A\nB")
+ list = RankKing::List.first
+
+ csrf_post "/lists/#{list.id}/axes", { name: " " }, from: "/lists/#{list.id}"
+ assert_equal 422, last_response.status
end
def test_compare_shows_a_pair