Add inline editing to series show page
Edit/Done toggle in the section header switches between static display
and inline editors for note, interval, and due date — same Alpine
components the old panel used. Urgency row highlighted in red when
overdue. Renamed "Due date" to "Next due date" for clarity.

Assisted-by: Claude Opus 4.6 via Claude Code
change opvwzyupzqznvyytvumywmtqowopmwsz
commit ce896206d0857c698d43557896e22006bd889681
author Alpha Chen <alpha@kejadlen.dev>
date
parent szvvmvwm
diff --git a/lib/ketchup/views/series/show.rb b/lib/ketchup/views/series/show.rb
index fcb932c..97f146b 100644
--- a/lib/ketchup/views/series/show.rb
+++ b/lib/ketchup/views/series/show.rb
@@ -19,11 +19,25 @@ module Views
         render Layout.new(current_user: @current_user, title: "#{note_title} — Ketchup", active_view: nil) do
           div(class: "dashboard") do
             div(class: "main-column") do
-              section(class: "section") do
+              section(class: "section", "x-data": "{ editing: false }") do
                 div(class: "section-header") do
                   h2(class: "section-title") do
                     span(class: "section-title-text") { "Series" }
                   end
+                  button(
+                    class: "section-edit-btn",
+                    "x-show": "!editing",
+                    "x-on:click": "editing = true; $dispatch('start-editing')"
+                  ) do
+                    plain "Edit"
+                  end
+                  button(
+                    class: "section-edit-btn",
+                    "x-show": "editing",
+                    "x-on:click": "editing = false; $dispatch('stop-editing')"
+                  ) do
+                    plain "Done"
+                  end
                 end
 
                 div(class: "series-note", id: "series-note-detail",
@@ -32,15 +46,53 @@ module Views
 
                 dl(class: "detail-fields") do
                   dt { "Repeat every" }
-                  dd { interval_text(@series.interval_count, @series.interval_unit) }
+                  dd("x-show": "!editing") do
+                    plain interval_text(@series.interval_count, @series.interval_unit)
+                  end
+                  dd(
+                    class: "detail-edit-interval",
+                    "x-show": "editing",
+                    "x-cloak": true,
+                    "x-data": "intervalEditor(#{@series.id}, #{@series.interval_count}, '#{@series.interval_unit}')"
+                  ) do
+                    input(
+                      type: "number",
+                      class: "detail-input detail-input-count",
+                      min: 1,
+                      "x-model.number": "count",
+                      "x-on:change": "save()"
+                    )
+                    select(
+                      class: "detail-input detail-input-unit",
+                      "x-model": "unit",
+                      "x-on:change": "save()"
+                    ) do
+                      INTERVAL_OPTIONS.each { |val, label| option(value: val) { label } }
+                    end
+                  end
 
                   if active_task
-                    dt { "Due date" }
-                    dd { active_task[:due_date].to_s }
+                    dt { "Next due date" }
+                    dd(
+                      "x-show": "!editing",
+                      "x-text": "new Date('#{active_task[:due_date]}T00:00').toLocaleDateString()"
+                    ) { active_task[:due_date].to_s }
+                    dd(
+                      "x-show": "editing",
+                      "x-cloak": true,
+                      "x-data": "dueDateEditor(#{@series.id}, '#{active_task[:due_date]}')"
+                    ) do
+                      input(
+                        type: "date",
+                        class: "detail-input detail-input-date",
+                        "x-model": "dueDate",
+                        "x-on:change": "save()"
+                      )
+                    end
 
                     if active_task.urgency > 0
-                      dt { "Urgency" }
-                      dd { "#{format("%.1f", active_task.urgency)}×" }
+                      dt(class: "detail-overdue") { "Urgency" }
+                      dd(class: "detail-overdue") { "#{format("%.1f", active_task.urgency)}×" }
                     end
                   end
                 end
diff --git a/public/css/app.css b/public/css/app.css
index ab7bac5..5957715 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -161,6 +161,18 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   color: #666;
 }
 
+.section-edit-btn {
+  all: unset;
+  order: 1;
+  font-size: var(--step--1);
+  color: #999;
+  cursor: pointer;
+}
+
+.section-edit-btn:hover {
+  color: #444;
+}
+
 .section-header::after {
   content: "";
   flex: 1;
@@ -464,7 +476,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .detail-fields {
   display: grid;
   grid-template-columns: auto 1fr;
-  gap: var(--space-3xs) var(--space-s);
+  gap: var(--space-2xs) var(--space-s);
   font-size: var(--step--2);
 }
 
@@ -477,8 +489,12 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   color: #444;
 }
 
+.detail-fields .detail-overdue {
+  color: #c00;
+}
+
 .detail-input {
-  font-size: var(--step--1);
+  font-size: inherit;
   padding: 2px 6px;
   margin: -3px -7px;
   border: 1px solid #ddd;
@@ -503,12 +519,14 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .detail-edit-interval {
-  display: flex;
-  gap: 4px;
+  display: grid;
+  grid-template-columns: 3.5em 1fr;
+  gap: var(--space-2xs);
 }
 
 .detail-input-count {
-  width: 3.5em;
+  width: 100%;
+  min-width: 0;
   -moz-appearance: textfield;
 }
 
@@ -519,7 +537,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .detail-input-unit {
-  flex: 1;
+  width: 100%;
   min-width: 0;
 }