Simplify overdue list headings and make cards fully clickable
Remove sort toggle (urgency/date) and Alpine sortable component.
Inline overdue count as "Overdue (4)" instead of separate badge.
Bump heading size and contrast for readability. Add "due" prefix
to overdue age text. Use stretched-link pattern so the entire
card area opens the series panel.

Assisted-by: Claude Opus 4.6 via Claude Code
change lmlkvryupqspzxzlqlmolzmlnyuvsuqz
commit 6018a31aa1529b25dd1027370785db5c0799030e
author Alpha Chen <alpha@kejadlen.dev>
date
parent ornnorsw
diff --git a/lib/ketchup/views/task_card.rb b/lib/ketchup/views/task_card.rb
index ab3adcb..1e8ae8d 100644
--- a/lib/ketchup/views/task_card.rb
+++ b/lib/ketchup/views/task_card.rb
@@ -27,8 +27,19 @@ module Views
         div(class: "task-body") do
           a(
             href: "/series/#{@task[:series_id]}",
-            class: "task-name",
-            "x-on:click.prevent": "$dispatch('open-panel', { seriesId: #{@task[:series_id]} })"
+            class: "task-name stretched-link",
+            "x-on:click.prevent": "
+              const panel = Alpine.$data(document.getElementById('panel'));
+              if (panel.open && panel.currentSeriesId === '#{@task[:series_id]}') {
+                panel.close();
+                history.pushState(null, '', '/');
+              } else {
+                panel.show('#{@task[:series_id]}');
+                history.pushState(null, '', '/series/#{@task[:series_id]}');
+              }
+              document.querySelectorAll('.task-card--selected').forEach(el => el.classList.remove('task-card--selected'));
+              if (panel.open) $el.closest('.task-card').classList.add('task-card--selected');
+            "
           ) { name }
           if @overdue
             span(class: "task-meta") do
@@ -37,8 +48,7 @@ module Views
           end
         end
         if @overdue && @task.urgency > 0
-          span(class: "task-urgency", "x-show": "sort === 'urgency'") { "#{format("%.1f", @task.urgency)}×" }
-          span(class: "task-date", "x-show": "sort === 'date'") { @task[:due_date].to_s }
+          span(class: "task-urgency") { "#{format("%.1f", @task.urgency)}×" }
         end
       end
     end
@@ -70,7 +80,7 @@ module Views
               "#{months} #{months == 1 ? "month" : "months"} ago"
             end
 
-      "every #{interval} · #{ago}"
+      "every #{interval} · due #{ago}"
     end
   end
 end
diff --git a/lib/ketchup/views/task_list.rb b/lib/ketchup/views/task_list.rb
index 7d84e37..620527b 100644
--- a/lib/ketchup/views/task_list.rb
+++ b/lib/ketchup/views/task_list.rb
@@ -14,7 +14,7 @@ module Views
     end
 
     def view_template
-      div(class: "task-list-container", "x-data": "sortable") do
+      div(class: "task-list-container", "x-data": "") do
         render_overdue
         render_upcoming
         render_completed_today
@@ -27,21 +27,13 @@ module Views
       section(class: "section section--overdue") do
         div(class: "section-header") do
           h2(class: "section-title") do
-            span(class: "section-title-text") { "Overdue" }
-            span(class: "section-count") { @overdue.size.to_s } unless @overdue.empty?
+            span(class: "section-title-text") do
+              plain "Overdue"
+              unless @overdue.empty?
+                plain " (#{@overdue.size})"
+              end
+            end
           end
-          nav(class: "sort-toggle") do
-            button(
-              "x-on:click": "sort = 'urgency'",
-              "x-bind:class": "sort === 'urgency' && 'sort-active'",
-              "x-bind:disabled": "sort === 'urgency'"
-            ) { "Urgency" }
-            button(
-              "x-on:click": "sort = 'date'",
-              "x-bind:class": "sort === 'date' && 'sort-active'",
-              "x-bind:disabled": "sort === 'date'"
-            ) { "Date" }
-          end unless @overdue.empty?
         end
 
         if @overdue.empty?
@@ -49,11 +41,7 @@ module Views
         else
           ul(class: "task-list") do
             @overdue.each do |task|
-              li(
-                class: "task-item",
-                "data-urgency": format("%.4f", task.urgency),
-                "data-due-date": task[:due_date].to_s
-              ) do
+              li(class: "task-item") do
                 render TaskCard.new(task: task, csrf: @csrf, selected: selected?(task), overdue: true)
               end
             end
diff --git a/public/css/app.css b/public/css/app.css
index 5b4cba0..a044a57 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -124,6 +124,8 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   max-width: 32rem;
   margin-inline: auto;
   padding: var(--space-m) var(--space-l);
+  position: relative;
+  z-index: 100;
 }
 
 /* -------------------- */
@@ -148,56 +150,24 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .section-title-text {
-  font-size: 10px;
-  font-weight: 500;
+  font-size: var(--step--1);
+  font-weight: 600;
   text-transform: uppercase;
-  letter-spacing: 0.12em;
-  color: #999;
+  letter-spacing: 0.08em;
+  color: #666;
 }
 
 .section-header::after {
   content: "";
   flex: 1;
   height: 1px;
-  background: #f0f0f0;
-}
-
-.section-count {
-  font-size: 10px;
-  font-weight: 500;
-  color: #c00;
-  font-variant-numeric: tabular-nums;
+  background: #ddd;
 }
 
 .section-header--overdue .section-title-text {
   color: #c00;
 }
 
-/* -------------------- */
-/* Sort toggle */
-/* -------------------- */
-
-.sort-toggle {
-  display: flex;
-  gap: var(--space-2xs);
-  font-size: var(--step--1);
-}
-
-.sort-toggle button {
-  all: unset;
-  color: #999;
-  cursor: default;
-}
-
-.sort-toggle button:hover {
-  color: #1a1a1a;
-}
-
-.sort-toggle button.sort-active {
-  color: #1a1a1a;
-  font-weight: 600;
-}
-
 /* -------------------- */
 /* Task list */
 /* -------------------- */
@@ -208,7 +178,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-item {
-  padding-block: var(--space-3xs);
 }
 
 .date-header {
@@ -241,20 +210,20 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   grid-template-columns: auto 1fr auto;
   align-items: baseline;
   gap: var(--space-xs);
-  padding: 7px 0;
+  padding: calc(7px + var(--space-3xs)) 0;
   cursor: pointer;
   transition: background 0.1s;
   position: relative;
 }
 
 .task-card:hover {
-  background: #f8f8f8;
+  background: #f0f0f0;
   margin-inline: -6px;
   padding-inline: 6px;
 }
 
 .task-card--selected {
-  background: #f5f5f5;
+  background: #ebebeb;
   margin-inline: -6px;
   padding-inline: 6px;
 }
@@ -264,7 +233,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-card--overdue:hover {
-  background: #fff5f5;
+  background: #ffe5e5;
 }
 
 .task-body {
@@ -276,6 +245,13 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   display: block;
 }
 
+.stretched-link::after {
+  content: "";
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+}
+
 .task-card--overdue .task-name {
   color: #c00;
   font-weight: 500;
@@ -311,14 +287,14 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .complete-btn {
-  width: 9px;
-  height: 9px;
+  width: 18px;
+  height: 18px;
   padding: 0;
   border: 1.5px solid #ccc;
-  border-radius: 50%;
+  border-radius: 3px;
   background: transparent;
   color: transparent;
-  font-size: 7px;
+  font-size: 14px;
   line-height: 1;
   cursor: pointer;
   transition: all 0.15s;
@@ -328,17 +304,18 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   flex-shrink: 0;
   position: relative;
   top: -1px;
+  z-index: 2;
 }
 
 .complete-btn:hover {
   border-color: #090;
   color: #090;
   background: #f0fff0;
+  font-weight: 900;
+  -webkit-text-stroke: 1px;
 }
 
 .task-card--overdue .complete-btn {
-  width: 10px;
-  height: 10px;
   border-color: #c00;
 }
 
diff --git a/public/js/app.js b/public/js/app.js
index 3351986..f82a75a 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -127,13 +127,14 @@ document.addEventListener("alpine:init", () => {
         if (userId) this.showUser(userId)
       }
 
-      // Listen for custom event from task cards
+      // Listen for custom event from other views (agenda, calendar)
       window.addEventListener("open-panel", (e) => {
         this.show(e.detail.seriesId)
       })
     },
 
     async show(seriesId) {
+      this.currentSeriesId = String(seriesId)
       this.loading = true
       this.open = true
       try {
@@ -147,6 +148,7 @@ document.addEventListener("alpine:init", () => {
     },
 
     async showUser(userId) {
+      this.currentSeriesId = null
       this.loading = true
       this.open = true
       try {
@@ -159,6 +161,7 @@ document.addEventListener("alpine:init", () => {
     },
 
     close() {
+      this.currentSeriesId = null
       this.open = false
       setTimeout(() => {
         if (this.$refs.content) this.$refs.content.innerHTML = ""
@@ -166,30 +169,6 @@ document.addEventListener("alpine:init", () => {
     },
   }))
 
-  Alpine.data("sortable", () => ({
-    sort: Alpine.$persist("urgency"),
-
-    init() {
-      this.$watch("sort", () => this.reorder())
-      if (this.sort !== "urgency") {
-        this.$nextTick(() => this.reorder())
-      }
-    },
-
-    reorder() {
-      const ul = this.$el.querySelector(".task-list")
-      if (!ul) return
-
-      const items = [...ul.querySelectorAll("li[data-urgency]")]
-      if (this.sort === "urgency") {
-        items.sort((a, b) => parseFloat(b.dataset.urgency) - parseFloat(a.dataset.urgency))
-      } else {
-        items.sort((a, b) => a.dataset.dueDate.localeCompare(b.dataset.dueDate))
-      }
-      items.forEach((li) => ul.appendChild(li))
-    },
-  }))
-
   Alpine.data("intervalEditor", (seriesId, initialCount, initialUnit) => ({
     count: initialCount,
     unit: initialUnit,