Replace user settings panel with standalone page
Same pattern as the series show page: full-page layout with section
header, Edit/Save toggle, and detail-fields. Removed the old panel
wrapper and form views. The hidden form holds just the CSRF token;
the email input links to it via the form attribute.

Assisted-by: Claude Opus 4.6 via Claude Code
change mkortwlyyynrqpvlqllsrlytovunrukr
commit a72b51c76c034f0c5dd13aa51b9ee1f0472d334d
author Alpha Chen <alpha@kejadlen.dev>
date
parent sxqsqqnq
diff --git a/lib/ketchup/views/series/show.rb b/lib/ketchup/views/series/show.rb
index 97f146b..96e62c4 100644
--- a/lib/ketchup/views/series/show.rb
+++ b/lib/ketchup/views/series/show.rb
@@ -36,7 +36,7 @@ module Views
                     "x-show": "editing",
                     "x-on:click": "editing = false; $dispatch('stop-editing')"
                   ) do
-                    plain "Done"
+                    plain "Save"
                   end
                 end
 
diff --git a/lib/ketchup/views/user/show.rb b/lib/ketchup/views/user/show.rb
new file mode 100644
index 0000000..564d06d
--- /dev/null
+++ b/lib/ketchup/views/user/show.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "phlex"
+
+require_relative "../layout"
+
+module Views
+  module User
+    class Show < Phlex::HTML
+      def initialize(current_user:, csrf:)
+        @current_user = current_user
+        @csrf = csrf
+      end
+
+      def view_template
+        email_path = "/users/#{@current_user[:id]}/email"
+        email = @current_user[:email]
+
+        render Layout.new(current_user: @current_user, title: "Settings — Ketchup", active_view: nil) do
+          div(class: "dashboard") do
+            div(class: "main-column") do
+              section(class: "section", "x-data": "{ editing: false }") do
+                div(class: "section-header") do
+                  h2(class: "section-title") do
+                    span(class: "section-title-text") { "User Settings" }
+                  end
+                  button(
+                    class: "section-edit-btn",
+                    "x-show": "!editing",
+                    "x-on:click": "editing = true"
+                  ) do
+                    plain "Edit"
+                  end
+                  button(
+                    class: "section-edit-btn",
+                    "x-show": "editing",
+                    "x-cloak": true,
+                    "x-on:click": "editing = false; document.getElementById('user-form').requestSubmit()"
+                  ) do
+                    plain "Save"
+                  end
+                end
+
+                form(method: "post", action: email_path, id: "user-form", style: "display:none") do
+                  input(type: "hidden", name: "_csrf", value: @csrf.call(email_path))
+                end
+
+                dl(class: "detail-fields") do
+                  dt { "Login" }
+                  dd { @current_user[:login] }
+
+                  dt { "Email" }
+                  dd("x-show": "!editing") do
+                    if email
+                      plain email
+                    else
+                      span(class: "detail-placeholder") { "not set" }
+                    end
+                  end
+                  dd("x-show": "editing", "x-cloak": true) do
+                    input(
+                      type: "email", name: "email",
+                      form: "user-form",
+                      class: "detail-input",
+                      value: email,
+                      placeholder: "for notifications"
+                    )
+                  end
+                end
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/ketchup/views/user_form.rb b/lib/ketchup/views/user_form.rb
deleted file mode 100644
index 79eee2b..0000000
--- a/lib/ketchup/views/user_form.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-module Views
-  class UserForm < Phlex::HTML
-    def initialize(current_user:, csrf:)
-      @current_user = current_user
-      @csrf = csrf
-    end
-
-    def view_template
-      email_path = "/users/#{@current_user[:id]}/email"
-      email = @current_user[:email]
-
-      div(class: "panel-inner", "x-data": "{ editing: false }") do
-        div(class: "panel-header") do
-          a(href: "/", class: "panel-close", "aria-label": "Close") { "←" }
-          div(class: "panel-actions") do
-            button(
-              class: "panel-action",
-              "x-show": "!editing",
-              "x-on:click": "editing = true"
-            ) { "Edit" }
-            button(
-              type: "submit",
-              form: "user-form",
-              class: "panel-action",
-              "x-show": "editing",
-              "x-cloak": true
-            ) { "Save" }
-          end
-        end
-
-        div(class: "panel-body") do
-          h2(class: "panel-body-title") { @current_user[:login] }
-
-          form(method: "post", action: email_path, id: "user-form") do
-            input(type: "hidden", name: "_csrf", value: @csrf.call(email_path))
-
-            dl(class: "detail-fields") do
-              dt { "Email" }
-              dd("x-show": "!editing") do
-                if email
-                  plain email
-                else
-                  span(class: "detail-placeholder") { "not set" }
-                end
-              end
-              dd("x-show": "editing", "x-cloak": true) do
-                input(
-                  type: "email", name: "email",
-                  class: "detail-input",
-                  value: email,
-                  placeholder: "for notifications"
-                )
-              end
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/ketchup/views/user_panel.rb b/lib/ketchup/views/user_panel.rb
deleted file mode 100644
index 405c0e2..0000000
--- a/lib/ketchup/views/user_panel.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-require "phlex"
-
-require_relative "user_form"
-
-module Views
-  class UserPanel < Phlex::HTML
-    def initialize(current_user:, csrf:)
-      @current_user = current_user
-      @csrf = csrf
-    end
-
-    def view_template
-      render UserForm.new(current_user: @current_user, csrf: @csrf)
-    end
-  end
-end
diff --git a/lib/ketchup/web.rb b/lib/ketchup/web.rb
index 5c75eae..27e6ddc 100644
--- a/lib/ketchup/web.rb
+++ b/lib/ketchup/web.rb
@@ -10,7 +10,7 @@ require_relative "views/calendar"
 require_relative "views/agenda"
 require_relative "views/series/new"
 require_relative "views/series/show"
-require_relative "views/user_panel"
+require_relative "views/user/show"
 
 class Web < Roda
   plugin :halt
@@ -77,12 +77,8 @@ class Web < Roda
     r.on "users", Integer do |user_id|
       r.halt 404 unless @user.id == user_id
 
-      r.get "panel" do
-        Views::UserPanel.new(current_user: @user, csrf: method(:csrf_token)).call
-      end
-
       r.get do
-        Views::Dashboard.new(current_user: @user, csrf: method(:csrf_token)).call
+        Views::User::Show.new(current_user: @user, csrf: method(:csrf_token)).call
       end
 
       r.post "email" do
diff --git a/test/test_web.rb b/test/test_web.rb
index 0ef34ae..11b77e4 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -423,31 +423,22 @@ class TestWeb < Minitest::Test
     assert_equal Date.new(2026, 3, 1), task[:due_date]
   end
 
-  def test_get_user_page_shows_dashboard
+  def test_get_user_page_shows_settings
     get "/", {}, auth_headers  # create user
     user_id = DB[:users].first(login: "alice@example.com")[:id]
 
     get "/users/#{user_id}", {}, auth_headers
     assert last_response.ok?
     assert_includes last_response.body, "alice@example.com"
-  end
-
-  def test_get_user_panel_shows_email_form
-    get "/", {}, auth_headers  # create user
-    user_id = DB[:users].first(login: "alice@example.com")[:id]
-
-    get "/users/#{user_id}/panel", {}, auth_headers
-    assert last_response.ok?
-    assert_includes last_response.body, "alice@example.com"
+    assert_includes last_response.body, "Settings"
     assert_includes last_response.body, 'name="email"'
-    refute_includes last_response.body, "<!DOCTYPE"
   end
 
   def test_post_user_email
     get "/", {}, auth_headers  # create user
     user_id = DB[:users].first(login: "alice@example.com")[:id]
 
-    get "/users/#{user_id}/panel", {}, auth_headers
+    get "/users/#{user_id}", {}, auth_headers
     token = last_response.body[/name="_csrf" value="([^"]+)"/, 1]
     post "/users/#{user_id}/email", { "_csrf" => token, email: "alice@example.org" }, auth_headers
     assert last_response.redirect?
@@ -460,7 +451,7 @@ class TestWeb < Minitest::Test
     get "/", {}, auth_headers
     user_id = DB[:users].first(login: "alice@example.com")[:id]
 
-    get "/users/#{user_id}/panel", {}, auth_headers
+    get "/users/#{user_id}", {}, auth_headers
     token = last_response.body[/name="_csrf" value="([^"]+)"/, 1]
     post "/users/#{user_id}/email", { "_csrf" => token, email: "" }, auth_headers