diff mrjunejune/src/jrpg/jrpg.js @ 273:e02e2036ef84 default tip

add Layer 2 JRPG component system Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Sat, 08 Aug 2026 02:08:08 -0700
parents 41a49c29a28f
children
line wrap: on
line diff
--- a/mrjunejune/src/jrpg/jrpg.js	Fri Aug 07 16:05:29 2026 -0700
+++ b/mrjunejune/src/jrpg/jrpg.js	Sat Aug 08 02:08:08 2026 -0700
@@ -543,6 +543,7 @@
 
 class MjjJrpgPreview extends HTMLElement {
   connectedCallback() {
+    this._modalOwner = this.querySelector("[data-detail-modal-owner]");
     this._dialog = this.querySelector("dialog");
     this._onDialogClose = () => {
       this._toolRequest = (this._toolRequest || 0) + 1;
@@ -570,30 +571,32 @@
       const latestBlog = event.target.closest("[data-latest-blog]");
       if (latestBlog && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
+        this.show("blog");
+        this._modalOwner?.open();
         void this.loadBlogDetail(latestBlog.dataset.blogUrl);
         return;
       }
       const blogArchive = event.target.closest("[data-blog-archive]");
       if (blogArchive) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
         this.show("blog");
+        this._modalOwner?.open();
         void this.loadBlogs();
         return;
       }
       const resumeModal = event.target.closest("[data-resume-modal]");
       if (resumeModal) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
         this.show("resume");
+        this._modalOwner?.open();
         void this.loadResume();
         return;
       }
       const latestTool = event.target.closest("[data-latest-tool]");
       if (latestTool && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
         event.preventDefault();
-        this.querySelector("zen-dialog").open();
+        this.show("tools");
+        this._modalOwner?.open();
         void this.loadToolDetail(latestTool.dataset.toolUrl);
       }
     };
@@ -608,6 +611,18 @@
     this.cleanupTool();
   }
 
+  _setDetailModalType(useWindow) {
+    const targetName = useWindow ? "mjj-window-modal" : "mjj-content-modal";
+    if (!this._modalOwner || this._modalOwner.localName === targetName) return;
+    const replacement = document.createElement(targetName);
+    for (const attribute of [...this._modalOwner.attributes]) {
+      replacement.setAttribute(attribute.name, attribute.value);
+    }
+    replacement.append(...this._modalOwner.childNodes);
+    this._modalOwner.replaceWith(replacement);
+    this._modalOwner = replacement;
+  }
+
   show(selection) {
     this.cleanupTool();
     const preview = PREVIEWS[selection] || PREVIEWS.resume;
@@ -617,9 +632,6 @@
     this.querySelector("[data-dialog-title]").textContent = preview.title;
     this.querySelector("[data-dialog-copy]").textContent = preview.copy;
     this.renderShowcase(preview.works);
-    const link = this.querySelector("[data-preview-link]");
-    link.href = preview.url;
-    link.setAttribute("aria-label", `Open ${preview.title}`);
     const resumeDossier = this.querySelector("[data-resume-dossier]");
     const resumeDownload = this.querySelector("[data-resume-download]");
     const blogBrowser = this.querySelector("[data-blog-browser]");
@@ -627,10 +639,10 @@
     const isResume = selection === "resume";
     const isBlog = selection === "blog";
     const isTools = selection === "tools";
+    this._setDetailModalType(isTools);
     this._dialog.dataset.detailMode = isTools ? "tools" : "content";
     resumeDossier.hidden = !isResume;
     resumeDownload.hidden = !isResume;
-    this.querySelector("[data-dialog-actions]").hidden = !isResume;
     blogBrowser.hidden = !isBlog;
     toolBrowser.hidden = !isTools;
     this.querySelector("[data-dialog-copy]").hidden =
@@ -711,6 +723,12 @@
       const resume = documentCopy.querySelector("main");
       if (!resume) throw new Error("Resume content is unavailable");
       sanitizeFetchedMain(resume);
+      for (const duplicateDownload of resume.querySelectorAll(
+        'a[href="/public/resume.pdf"]',
+      )) {
+        duplicateDownload.closest("zen-button")?.remove();
+        duplicateDownload.remove();
+      }
       content.replaceChildren(...resume.childNodes);
       status.hidden = true;
       this._resumeLoaded = true;
@@ -823,9 +841,6 @@
     status.textContent = `Loading ${blog.label}...`;
     content.replaceChildren();
     this.querySelector("[data-dialog-title]").textContent = blog.label;
-    const fullPage = this.querySelector("[data-preview-link]");
-    fullPage.href = blog.url;
-    fullPage.setAttribute("aria-label", `Open ${blog.label}`);
     for (const button of this.querySelectorAll("[data-blog-entry]")) {
       button.setAttribute(
         "aria-pressed",
@@ -928,9 +943,6 @@
     this.cleanupTool();
     content.replaceChildren();
     this.querySelector("[data-dialog-title]").textContent = tool.label;
-    const fullPage = this.querySelector("[data-preview-link]");
-    fullPage.href = tool.url;
-    fullPage.setAttribute("aria-label", `Open ${tool.label}`);
     if (tool.url === "/tools/markdown_to_html") {
       await this.renderMarkdownTool(content, status, request);
       return;
@@ -1426,10 +1438,12 @@
     this._loadMoreButton = this.querySelector("[data-archive-load-more]");
     this._newButton = this.querySelector("[data-archive-new]");
     this._closeButton = this.querySelector("[data-archive-close]");
+    this._deleteModal = this.querySelector("[data-archive-delete-owner]");
     this._deleteDialog = this.querySelector("[data-archive-delete-dialog]");
     this._deleteNameEl = this.querySelector("[data-archive-delete-name]");
     this._deleteCancelButton = this.querySelector("[data-archive-delete-cancel]");
     this._deleteConfirmButton = this.querySelector("[data-archive-delete-confirm]");
+    this._renameModal = this.querySelector("[data-archive-rename-owner]");
     this._renameDialog = this.querySelector("[data-archive-rename-dialog]");
     this._renameInput = this.querySelector("[data-archive-rename-input]");
     this._renameForm = this.querySelector("[data-archive-rename-form]");
@@ -1452,7 +1466,7 @@
     });
 
     this._deleteCancelButton?.addEventListener("click", () => {
-      this._deleteDialog?.close();
+      this._deleteModal?.close();
       const t = this._pendingDeleteTrigger;
       this._pendingDeleteId = null;
       this._pendingDeleteTrigger = null;
@@ -1462,7 +1476,7 @@
     this._deleteConfirmButton?.addEventListener("click", () => {
       const id = this._pendingDeleteId;
       const trigger = this._pendingDeleteTrigger;
-      this._deleteDialog?.close();
+      this._deleteModal?.close();
       this._pendingDeleteId = null;
       this._pendingDeleteTrigger = null;
       if (id) {
@@ -1481,7 +1495,7 @@
     });
 
     this._renameCancelButton?.addEventListener("click", () => {
-      this._renameDialog?.close();
+      this._renameModal?.close();
       const t = this._pendingRenameTrigger;
       this._pendingRenameId = null;
       this._pendingRenameTrigger = null;
@@ -1494,7 +1508,7 @@
       const trigger = this._pendingRenameTrigger;
       const newTitle = this._renameInput?.value.trim();
       if (!id || !newTitle) return;
-      this._renameDialog?.close();
+      this._renameModal?.close();
       this._pendingRenameId = null;
       this._pendingRenameTrigger = null;
       this.dispatchEvent(new CustomEvent("mjj-archive-rename", {
@@ -1529,7 +1543,7 @@
           this._pendingRenameId = item._convId;
           this._pendingRenameTrigger = renameButton;
           if (this._renameInput) this._renameInput.value = item._convTitle || "";
-          this._renameDialog?.showModal();
+          this._renameModal?.open();
           requestAnimationFrame(() => { this._renameInput?.select(); });
         }
         return;
@@ -1541,7 +1555,7 @@
           this._pendingDeleteId = item._convId;
           this._pendingDeleteTrigger = deleteButton;
           if (this._deleteNameEl) this._deleteNameEl.textContent = item._convTitle || "";
-          this._deleteDialog?.showModal();
+          this._deleteModal?.open();
           requestAnimationFrame(() => { this._deleteConfirmButton?.focus(); });
         }
         return;
@@ -2186,7 +2200,10 @@
 
   let characterTimer = 0;
   let currentConversationId = null;
-  let currentPanel = "resume";
+  const serverInitialPanel = VALID_PANELS.includes(shell?.dataset.initialPanel)
+    ? shell.dataset.initialPanel
+    : "resume";
+  let currentPanel = serverInitialPanel;
   let archiveCursor = null;
   let activeController = null;
   let archiveLoadInFlight = false;  /* serialize pagination requests */
@@ -2799,7 +2816,7 @@
         normalUrl,
       );
     }
-    currentPanel = validPanel || "resume";
+    currentPanel = validPanel || serverInitialPanel;
   }
 
   /* ---- Load archive; only an explicit URL restores a conversation ---- */