Switch panel to client-side fetch, extract initPanelEditors
Panel fetches content into persistent shell instead of relying on
server-rendered HTML. OverType setup moved to initPanelEditors() so
it runs after fetch injects the panel DOM.

Assisted-by: Claude Opus 4.6 via Claude Code
change yumrkzwtvqruyrvkkzqmwwontsnzlsqo
commit 0d5e029f6d2077c5edf4f7c59fbbfdb96f8746ce
author Alpha Chen <alpha@kejadlen.dev>
date
parent yymxqoro
diff --git a/lib/ketchup/views/task_card.rb b/lib/ketchup/views/task_card.rb
index 51b6d7c..ab3adcb 100644
--- a/lib/ketchup/views/task_card.rb
+++ b/lib/ketchup/views/task_card.rb
@@ -25,7 +25,11 @@ module Views
           ) { "✓" }
         end
         div(class: "task-body") do
-          a(href: "/series/#{@task[:series_id]}", class: "task-name") { name }
+          a(
+            href: "/series/#{@task[:series_id]}",
+            class: "task-name",
+            "x-on:click.prevent": "$dispatch('open-panel', { seriesId: #{@task[:series_id]} })"
+          ) { name }
           if @overdue
             span(class: "task-meta") do
               plain meta_text
diff --git a/public/js/app.js b/public/js/app.js
index 492cae1..3351986 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -58,22 +58,111 @@ function saveSeriesField(seriesId, field, value) {
 
 OverType.setTheme({ name: "ketchup", colors: { text: "#1a1a1a" } })
 
+function initPanelEditors(container) {
+  const noteDetail = container.querySelector("#series-note-detail")
+  if (noteDetail) {
+    const seriesId = noteDetail.dataset.seriesId
+    const initialNote = noteDetail.dataset.value || ""
+
+    const [editor] = new OverType(noteDetail, {
+      value: initialNote,
+      placeholder: "Series note...",
+      autoResize: true,
+      minHeight: 14,
+      padding: "0 4px",
+    })
+
+    const resizeNote = compactOverType(noteDetail)
+
+    const ta = noteDetail.querySelector("textarea")
+    if (ta) {
+      ta.style.pointerEvents = "none"
+      ta.readOnly = true
+
+      ta.addEventListener("blur", () => {
+        const note = editor.getValue().trim()
+        if (note === (initialNote || "").trim()) return
+        saveSeriesField(seriesId, "note", note).then(() => location.reload())
+      })
+
+      container.addEventListener("start-editing", () => {
+        ta.style.pointerEvents = ""
+        ta.readOnly = false
+        ta.focus()
+        if (resizeNote) requestAnimationFrame(resizeNote)
+      })
+
+      container.addEventListener("stop-editing", () => {
+        const note = editor.getValue().trim()
+        if (note !== (initialNote || "").trim()) {
+          saveSeriesField(seriesId, "note", note).then(() => location.reload())
+          return
+        }
+        ta.style.pointerEvents = "none"
+        ta.readOnly = true
+        if (resizeNote) requestAnimationFrame(resizeNote)
+      })
+    }
+  }
+}
+
 document.addEventListener("alpine:init", () => {
   // Panel open/close
   Alpine.data("panel", () => ({
     open: false,
+    loading: false,
 
     init() {
-      // Open panel if server rendered content into it
-      if (this.$el.querySelector(".panel-inner")) {
-        this.$nextTick(() => { this.open = true })
+      // Auto-open panel if server set data-open-series on the dashboard
+      const dashboard = document.querySelector("[data-open-series]")
+      if (dashboard) {
+        const seriesId = dashboard.dataset.openSeries
+        if (seriesId) this.show(seriesId)
+      }
+
+      // Auto-open user panel
+      const userDash = document.querySelector("[data-open-user]")
+      if (userDash) {
+        const userId = userDash.dataset.openUser
+        if (userId) this.showUser(userId)
+      }
+
+      // Listen for custom event from task cards
+      window.addEventListener("open-panel", (e) => {
+        this.show(e.detail.seriesId)
+      })
+    },
+
+    async show(seriesId) {
+      this.loading = true
+      this.open = true
+      try {
+        const resp = await fetch(`/series/${seriesId}/panel`)
+        if (!resp.ok) return
+        this.$refs.content.innerHTML = await resp.text()
+        initPanelEditors(this.$refs.content)
+      } finally {
+        this.loading = false
+      }
+    },
+
+    async showUser(userId) {
+      this.loading = true
+      this.open = true
+      try {
+        const resp = await fetch(`/users/${userId}/panel`)
+        if (!resp.ok) return
+        this.$refs.content.innerHTML = await resp.text()
+      } finally {
+        this.loading = false
       }
     },
 
     close() {
       this.open = false
-      // Navigate back to root after close animation
-      setTimeout(() => { window.location.href = "/" }, 250)
+      setTimeout(() => {
+        if (this.$refs.content) this.$refs.content.innerHTML = ""
+      }, 250)
     },
   }))
 
@@ -177,53 +266,6 @@ document.addEventListener("alpine:init", () => {
     },
   }))
 
-  // Series note detail editor — initialized when a series is selected
-  const noteDetail = document.getElementById("series-note-detail")
-  if (noteDetail) {
-    const seriesId = noteDetail.dataset.seriesId
-    const initialNote = noteDetail.dataset.value || ""
-
-    const [editor] = new OverType(noteDetail, {
-      value: initialNote,
-      placeholder: "Series note...",
-      autoResize: true,
-      minHeight: 14,
-      padding: "0 4px",
-    })
-
-    const resizeNote = compactOverType(noteDetail)
-
-    const ta = noteDetail.querySelector("textarea")
-    if (ta) {
-      ta.style.pointerEvents = "none"
-      ta.readOnly = true
-
-      ta.addEventListener("blur", () => {
-        const note = editor.getValue().trim()
-        if (note === (initialNote || "").trim()) return
-        saveSeriesField(seriesId, "note", note).then(() => location.reload())
-      })
-
-      document.addEventListener("start-editing", () => {
-        ta.style.pointerEvents = ""
-        ta.readOnly = false
-        ta.focus()
-        if (resizeNote) requestAnimationFrame(resizeNote)
-      })
-
-      document.addEventListener("stop-editing", () => {
-        const note = editor.getValue().trim()
-        if (note !== (initialNote || "").trim()) {
-          saveSeriesField(seriesId, "note", note).then(() => location.reload())
-          return
-        }
-        ta.style.pointerEvents = "none"
-        ta.readOnly = true
-        if (resizeNote) requestAnimationFrame(resizeNote)
-      })
-    }
-  }
-
   // New series form editor
   const newNoteEl = document.getElementById("series-note-editor")
   if (newNoteEl) {