annotate mrjunejune/src/public/mjj-modal.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
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
273
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
1 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
2 * Layer 2 modal composites.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
3 *
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
4 * Dependencies flow downward only: Zenbu Layer 1 primitives and Layer 0
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
5 * themes/i18n/API mapping. Application routes and state remain outside.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
6 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
7
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
8 let nextModalId = 1;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
9
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
10 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
11 * Validates and labels a site modal composed with zen-dialog.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
12 *
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
13 * @extends {HTMLElement}
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
14 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
15 class MjjModal extends HTMLElement {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
16 connectedCallback() {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
17 this._observer ||= new MutationObserver(() => this.sync());
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
18 this._observer.observe(this, { childList: true, subtree: true });
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
19 this.sync();
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
20 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
21
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
22 disconnectedCallback() {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
23 this._observer?.disconnect();
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
24 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
25
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
26 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
27 * Applies the strict modal region contract.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
28 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
29 sync() {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
30 const owner = this.querySelector(":scope > zen-dialog");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
31 const dialog = owner?.querySelector(":scope > dialog");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
32 const header = dialog?.querySelector(":scope > [data-modal-header]");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
33 const body = dialog?.querySelector(":scope > [data-modal-body]");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
34 const footer = dialog?.querySelector(":scope > [data-modal-footer]");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
35 const title = header?.querySelector("[data-modal-title]");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
36 const regions = dialog
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
37 ? [...dialog.children].filter(element =>
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
38 element.matches(
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
39 "[data-modal-header], [data-modal-body], [data-modal-footer]",
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
40 ))
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
41 : [];
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
42 const regionOrder = regions.map(element => {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
43 if (element.hasAttribute("data-modal-header")) return "header";
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
44 if (element.hasAttribute("data-modal-body")) return "body";
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
45 return "footer";
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
46 });
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
47 const expectedOrder = footer
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
48 ? ["header", "body", "footer"]
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
49 : ["header", "body"];
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
50 const errors = [];
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
51
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
52 if (!owner) errors.push("requires one direct zen-dialog");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
53 if (!dialog) errors.push("requires one direct native dialog");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
54 if (!header) errors.push("requires a direct data-modal-header region");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
55 if (!body) errors.push("requires a direct data-modal-body region");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
56 if (!title) errors.push("requires data-modal-title inside the header");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
57 if (regionOrder.join(",") !== expectedOrder.join(",")) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
58 errors.push(`regions must be ordered ${expectedOrder.join(", ")}`);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
59 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
60
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
61 if (errors.length) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
62 this.dataset.invalid = "";
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
63 this.removeAttribute("data-ready");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
64 const message = `${this.localName}: ${errors.join("; ")}`;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
65 if (this._lastError !== message) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
66 this._lastError = message;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
67 console.error(message);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
68 this.dispatchEvent(new CustomEvent("mjj-modal-error", {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
69 bubbles: true,
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
70 detail: { errors: [...errors] },
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
71 }));
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
72 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
73 return;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
74 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
75
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
76 delete this.dataset.invalid;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
77 this.dataset.ready = "";
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
78 this.dataset.componentLayer = "2";
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
79 this._lastError = null;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
80 if (!title.id) title.id = `mjj-modal-title-${nextModalId++}`;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
81 dialog.setAttribute("aria-labelledby", title.id);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
82 dialog.setAttribute("aria-modal", "true");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
83 const description = body.querySelector("[data-modal-description]");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
84 if (description) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
85 if (!description.id) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
86 description.id = `mjj-modal-description-${nextModalId++}`;
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
87 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
88 dialog.setAttribute("aria-describedby", description.id);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
89 } else {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
90 dialog.removeAttribute("aria-describedby");
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
91 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
92 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
93
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
94 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
95 * Opens the composed Zenbu dialog.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
96 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
97 open() {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
98 this.querySelector(":scope > zen-dialog")?.open();
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
99 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
100
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
101 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
102 * Closes the composed Zenbu dialog.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
103 *
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
104 * @param {string=} returnValue
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
105 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
106 close(returnValue) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
107 this.querySelector(":scope > zen-dialog")?.close(returnValue);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
108 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
109 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
110
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
111 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
112 * Presents long-form editorial content with a reading-focused body.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
113 *
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
114 * @extends {MjjModal}
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
115 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
116 export class MjjContentModal extends MjjModal {}
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
117
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
118 /**
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
119 * Presents application content in a strict window-style frame.
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
120 *
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
121 * @extends {MjjModal}
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
122 */
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
123 export class MjjWindowModal extends MjjModal {}
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
124
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
125 if (!customElements.get("mjj-content-modal")) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
126 customElements.define("mjj-content-modal", MjjContentModal);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
127 }
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
128 if (!customElements.get("mjj-window-modal")) {
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
129 customElements.define("mjj-window-modal", MjjWindowModal);
e02e2036ef84 add Layer 2 JRPG component system
MrJuneJune <me@mrjunejune.com>
parents:
diff changeset
130 }