From 7b8102c242d72d3747ff6284179873bcbe93bf8e Mon Sep 17 00:00:00 2001
From: Sli <antoine@bartuccio.fr>
Date: Tue, 10 Jun 2025 23:08:04 +0200
Subject: [PATCH] Add lit-html and use it for ics-calendar popups

---
 .../com/components/ics-calendar-index.ts      | 113 +++----
 package-lock.json                             | 302 +++++++++++-------
 package.json                                  |   1 +
 3 files changed, 237 insertions(+), 179 deletions(-)

diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts
index ad85280a..bc2ec9b4 100644
--- a/com/static/bundled/com/components/ics-calendar-index.ts
+++ b/com/static/bundled/com/components/ics-calendar-index.ts
@@ -7,6 +7,7 @@ import frLocale from "@fullcalendar/core/locales/fr";
 import dayGridPlugin from "@fullcalendar/daygrid";
 import iCalendarPlugin from "@fullcalendar/icalendar";
 import listPlugin from "@fullcalendar/list";
+import { type HTMLTemplateResult, html, render } from "lit-html";
 import {
   calendarCalendarInternal,
   calendarCalendarUnpublished,
@@ -176,29 +177,25 @@ export class IcsCalendar extends inheritHtmlElement("div") {
       oldPopup.remove();
     }
 
-    const makePopupInfo = (info: HTMLElement, iconClass: string) => {
-      const row = document.createElement("div");
-      const icon = document.createElement("i");
-
-      row.setAttribute("class", "event-details-row");
-
-      icon.setAttribute("class", `event-detail-row-icon fa-xl ${iconClass}`);
-
-      row.appendChild(icon);
-      row.appendChild(info);
-
-      return row;
+    const makePopupInfo = (info: HTMLTemplateResult, iconClass: string) => {
+      return html`
+        <div class="event-details-row">
+          <i class="event-detail-row-icon fa-xl ${iconClass}"></i>
+          ${info}
+        </div>
+      `;
     };
 
     const makePopupTitle = (event: EventImpl) => {
-      const row = document.createElement("div");
-      row.innerHTML = `
-        <h4 class="event-details-row-content">
-          ${event.title}
-        </h4>
-        <span class="event-details-row-content">
-          ${this.formatDate(event.start)} - ${this.formatDate(event.end)}
-        </span>
+      const row = html`
+        <div>
+          <h4 class="event-details-row-content">
+            ${event.title}
+          </h4>
+          <span class="event-details-row-content">
+            ${this.formatDate(event.start)} - ${this.formatDate(event.end)}
+          </span>
+        </div>
       `;
       return makePopupInfo(
         row,
@@ -210,9 +207,11 @@ export class IcsCalendar extends inheritHtmlElement("div") {
       if (event.extendedProps.location === null) {
         return null;
       }
-      const info = document.createElement("div");
-      info.innerText = event.extendedProps.location;
-
+      const info = html`
+        <div>
+          ${event.extendedProps.location}
+        </div>
+      `;
       return makePopupInfo(info, "fa-solid fa-location-dot");
     };
 
@@ -220,10 +219,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
       if (event.url === "") {
         return null;
       }
-      const url = document.createElement("a");
-      url.href = event.url;
-      url.textContent = gettext("More info");
-
+      const url = html`<a href="${event.url}">${gettext("More info")}</a>`;
       return makePopupInfo(url, "fa-solid fa-link");
     };
 
@@ -232,64 +228,59 @@ export class IcsCalendar extends inheritHtmlElement("div") {
         return null;
       }
       const newsId = this.getNewsId(event);
-      const div = document.createElement("div");
+      const buttons = [] as HTMLTemplateResult[];
+
       if (this.canModerate) {
         if (event.source.internalEventSource.ui.classNames.includes("unpublished")) {
-          const button = document.createElement("button");
-          button.innerHTML = `<i class="fa fa-check"></i>${gettext("Publish")}`;
-          button.setAttribute("class", "btn btn-green");
-          button.onclick = () => {
-            this.publishNews(newsId);
-          };
-          div.appendChild(button);
+          const button = html`
+            <button class="btn btn-green" @click="${() => this.publishNews(newsId)}">
+              <i class="fa fa-check"></i>${gettext("Publish")}
+            </button>
+          `;
+          buttons.push(button);
         } else {
-          const button = document.createElement("button");
-          button.innerHTML = `<i class="fa fa-times"></i>${gettext("Unpublish")}`;
-          button.setAttribute("class", "btn btn-orange");
-          button.onclick = () => {
-            this.unpublishNews(newsId);
-          };
-          div.appendChild(button);
+          const button = html`
+            <button class="btn btn-orange" @click="${() => this.unpublishNews(newsId)}">
+              <i class="fa fa-times"></i>${gettext("Unpublish")}
+            </button>
+          `;
+          buttons.push(button);
         }
       }
       if (this.canDelete) {
-        const button = document.createElement("button");
-        button.innerHTML = `<i class="fa fa-trash-can"></i>${gettext("Delete")}`;
-        button.setAttribute("class", "btn btn-red");
-        button.onclick = () => {
-          this.deleteNews(newsId);
-        };
-        div.appendChild(button);
+        const button = html`
+          <button class="btn btn-red" @click="${() => this.deleteNews(newsId)}">
+            <i class="fa fa-trash-can"></i>${gettext("Delete")}
+          </button>
+        `;
+        buttons.push(button);
       }
 
-      return makePopupInfo(div, "fa-solid fa-toolbox");
+      return makePopupInfo(html`<div>${buttons}</div>`, "fa-solid fa-toolbox");
     };
 
     // Create new popup
-    const popup = document.createElement("div");
-    const popupContainer = document.createElement("div");
-
-    popup.setAttribute("id", "event-details");
-    popupContainer.setAttribute("class", "event-details-container");
-
-    popupContainer.appendChild(makePopupTitle(event.event));
+    const infos = [] as HTMLTemplateResult[];
+    infos.push(makePopupTitle(event.event));
 
     const location = makePopupLocation(event.event);
     if (location !== null) {
-      popupContainer.appendChild(location);
+      infos.push(location);
     }
 
     const url = makePopupUrl(event.event);
     if (url !== null) {
-      popupContainer.appendChild(url);
+      infos.push(url);
     }
 
     const tools = makePopupTools(event.event);
     if (tools !== null) {
-      popupContainer.appendChild(tools);
+      infos.push(tools);
     }
 
-    popup.appendChild(popupContainer);
+    const popup = document.createElement("div");
+    popup.setAttribute("id", "event-details");
+    render(html`<div class="event-details-container">${infos}</div>`, popup);
 
     // We can't just add the element relative to the one we want to appear under
     // Otherwise, it either gets clipped by the boundaries of the calendar or resize cells
diff --git a/package-lock.json b/package-lock.json
index 87d4bc5c..0aea6a29 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,7 @@
         "jquery": "^3.7.1",
         "jquery-ui": "^1.14.0",
         "js-cookie": "^3.0.5",
+        "lit-html": "^3.3.0",
         "native-file-system-adapter": "^3.0.1",
         "three": "^0.172.0",
         "three-spritetext": "^1.9.0",
@@ -79,15 +80,15 @@
       "integrity": "sha512-Uu3CYSvFrNdDkYKEaEKHAk0decaxVFlSSqf50Okte/9vJjO2rESzPF1ngQjS9H1aX45RIXRGMYOXJ/LPDFwUdQ=="
     },
     "node_modules/@babel/code-frame": {
-      "version": "7.26.2",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
-      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+      "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.27.1",
         "js-tokens": "^4.0.0",
-        "picocolors": "^1.0.0"
+        "picocolors": "^1.1.1"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -358,9 +359,9 @@
       }
     },
     "node_modules/@babel/helper-string-parser": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
-      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -368,9 +369,9 @@
       }
     },
     "node_modules/@babel/helper-validator-identifier": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
-      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+      "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -403,27 +404,27 @@
       }
     },
     "node_modules/@babel/helpers": {
-      "version": "7.26.0",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
-      "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+      "version": "7.27.6",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
+      "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/template": "^7.25.9",
-        "@babel/types": "^7.26.0"
+        "@babel/template": "^7.27.2",
+        "@babel/types": "^7.27.6"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.26.5",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
-      "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
+      "version": "7.27.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
+      "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/types": "^7.26.5"
+        "@babel/types": "^7.27.3"
       },
       "bin": {
         "parser": "bin/babel-parser.js"
@@ -1527,15 +1528,15 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.25.9",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
-      "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+      "version": "7.27.2",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+      "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/code-frame": "^7.25.9",
-        "@babel/parser": "^7.25.9",
-        "@babel/types": "^7.25.9"
+        "@babel/code-frame": "^7.27.1",
+        "@babel/parser": "^7.27.2",
+        "@babel/types": "^7.27.1"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -1561,14 +1562,14 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.26.5",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
-      "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
+      "version": "7.27.6",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
+      "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-string-parser": "^7.25.9",
-        "@babel/helper-validator-identifier": "^7.25.9"
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.27.1"
       },
       "engines": {
         "node": ">=6.9.0"
@@ -2469,9 +2470,9 @@
       }
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
-      "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz",
+      "integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==",
       "cpu": [
         "arm"
       ],
@@ -2483,9 +2484,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
-      "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz",
+      "integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==",
       "cpu": [
         "arm64"
       ],
@@ -2497,9 +2498,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
-      "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz",
+      "integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==",
       "cpu": [
         "arm64"
       ],
@@ -2511,9 +2512,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
-      "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz",
+      "integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==",
       "cpu": [
         "x64"
       ],
@@ -2525,9 +2526,9 @@
       ]
     },
     "node_modules/@rollup/rollup-freebsd-arm64": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
-      "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz",
+      "integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==",
       "cpu": [
         "arm64"
       ],
@@ -2539,9 +2540,9 @@
       ]
     },
     "node_modules/@rollup/rollup-freebsd-x64": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
-      "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz",
+      "integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==",
       "cpu": [
         "x64"
       ],
@@ -2553,9 +2554,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
-      "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz",
+      "integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==",
       "cpu": [
         "arm"
       ],
@@ -2567,9 +2568,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
-      "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz",
+      "integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==",
       "cpu": [
         "arm"
       ],
@@ -2581,9 +2582,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
-      "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz",
+      "integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==",
       "cpu": [
         "arm64"
       ],
@@ -2595,9 +2596,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
-      "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz",
+      "integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==",
       "cpu": [
         "arm64"
       ],
@@ -2609,9 +2610,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
-      "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz",
+      "integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==",
       "cpu": [
         "loong64"
       ],
@@ -2623,9 +2624,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
-      "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz",
+      "integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==",
       "cpu": [
         "ppc64"
       ],
@@ -2637,9 +2638,23 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
-      "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz",
+      "integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz",
+      "integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==",
       "cpu": [
         "riscv64"
       ],
@@ -2651,9 +2666,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
-      "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz",
+      "integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==",
       "cpu": [
         "s390x"
       ],
@@ -2665,9 +2680,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
-      "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz",
+      "integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==",
       "cpu": [
         "x64"
       ],
@@ -2679,9 +2694,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
-      "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz",
+      "integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==",
       "cpu": [
         "x64"
       ],
@@ -2693,9 +2708,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
-      "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz",
+      "integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==",
       "cpu": [
         "arm64"
       ],
@@ -2707,9 +2722,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
-      "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz",
+      "integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==",
       "cpu": [
         "ia32"
       ],
@@ -2721,9 +2736,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
-      "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz",
+      "integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==",
       "cpu": [
         "x64"
       ],
@@ -2832,9 +2847,9 @@
       }
     },
     "node_modules/@types/estree": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
-      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+      "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
       "license": "MIT"
     },
     "node_modules/@types/jquery": {
@@ -2876,6 +2891,12 @@
         "@types/estree": "*"
       }
     },
+    "node_modules/@types/trusted-types": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+      "license": "MIT"
+    },
     "node_modules/@vue/reactivity": {
       "version": "3.1.5",
       "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
@@ -3833,6 +3854,21 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/fdir": {
+      "version": "6.4.6",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+      "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/fetch-blob": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -4415,6 +4451,15 @@
       "integrity": "sha512-WUNxuO7O79TEkxCj6OIaK5TJBkaWaR/IKNTakgV9PwDn+mrr63MLHed34AcE2yTaDntgO6l0zGFIzhcoTeroTA==",
       "license": "EPL-1.0"
     },
+    "node_modules/lit-html": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.0.tgz",
+      "integrity": "sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@types/trusted-types": "^2.0.2"
+      }
+    },
     "node_modules/lodash-es": {
       "version": "4.17.21",
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
@@ -5173,13 +5218,13 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.30.1",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
-      "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
+      "version": "4.42.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz",
+      "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@types/estree": "1.0.6"
+        "@types/estree": "1.0.7"
       },
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -5189,25 +5234,26 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.30.1",
-        "@rollup/rollup-android-arm64": "4.30.1",
-        "@rollup/rollup-darwin-arm64": "4.30.1",
-        "@rollup/rollup-darwin-x64": "4.30.1",
-        "@rollup/rollup-freebsd-arm64": "4.30.1",
-        "@rollup/rollup-freebsd-x64": "4.30.1",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
-        "@rollup/rollup-linux-arm-musleabihf": "4.30.1",
-        "@rollup/rollup-linux-arm64-gnu": "4.30.1",
-        "@rollup/rollup-linux-arm64-musl": "4.30.1",
-        "@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
-        "@rollup/rollup-linux-riscv64-gnu": "4.30.1",
-        "@rollup/rollup-linux-s390x-gnu": "4.30.1",
-        "@rollup/rollup-linux-x64-gnu": "4.30.1",
-        "@rollup/rollup-linux-x64-musl": "4.30.1",
-        "@rollup/rollup-win32-arm64-msvc": "4.30.1",
-        "@rollup/rollup-win32-ia32-msvc": "4.30.1",
-        "@rollup/rollup-win32-x64-msvc": "4.30.1",
+        "@rollup/rollup-android-arm-eabi": "4.42.0",
+        "@rollup/rollup-android-arm64": "4.42.0",
+        "@rollup/rollup-darwin-arm64": "4.42.0",
+        "@rollup/rollup-darwin-x64": "4.42.0",
+        "@rollup/rollup-freebsd-arm64": "4.42.0",
+        "@rollup/rollup-freebsd-x64": "4.42.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.42.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.42.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.42.0",
+        "@rollup/rollup-linux-arm64-musl": "4.42.0",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.42.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.42.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.42.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.42.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.42.0",
+        "@rollup/rollup-linux-x64-gnu": "4.42.0",
+        "@rollup/rollup-linux-x64-musl": "4.42.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.42.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.42.0",
+        "@rollup/rollup-win32-x64-msvc": "4.42.0",
         "fsevents": "~2.3.2"
       }
     },
@@ -5563,6 +5609,23 @@
       "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
       "license": "MIT"
     },
+    "node_modules/tinyglobby": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+      "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
     "node_modules/tmp": {
       "version": "0.2.3",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -5731,15 +5794,18 @@
       }
     },
     "node_modules/vite": {
-      "version": "6.2.5",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
-      "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
+      "version": "6.3.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+      "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
         "postcss": "^8.5.3",
-        "rollup": "^4.30.1"
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
       },
       "bin": {
         "vite": "bin/vite.js"
diff --git a/package.json b/package.json
index 22b1d2c1..474e0cc6 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
     "jquery": "^3.7.1",
     "jquery-ui": "^1.14.0",
     "js-cookie": "^3.0.5",
+    "lit-html": "^3.3.0",
     "native-file-system-adapter": "^3.0.1",
     "three": "^0.172.0",
     "three-spritetext": "^1.9.0",