Most of CC flow complete. Refactored css. Code is now a mess. Navbar > Sidebar. CSS still hates me move to tailwind 🙏

This commit is contained in:
boris committed 2026-02-25 23:53:08 +00:00
1 parent 8be4c8d571
commit a73c5ea002
10 files changed
+10331 -195

No files matched your search

+285 -26
View File
@@ -39,13 +39,26 @@ const MIME_TYPES: Record<string, string> = {
".xml": "application/xml; charset=utf-8"
};
const CORS_HEADERS: Record<string, string> = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
};
function withCorsHeaders(headers: Record<string, string>): Record<string, string> {
return {
...CORS_HEADERS,
...headers
};
}
function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
return `
(() => {
${externalImageRoot ? `globalThis.DEPLOYED_IMG_ROOT = ${JSON.stringify(externalImageRoot)};` : ""}
const root = document.documentElement;
root.classList.add("desktop-reskin");
root.classList.add("desktop-reskin", "desktop-app-navbar-mode");
${
SHOULD_FORCE_NIGHT_THEME
@@ -69,6 +82,15 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
console.info("[5ET-D]", content);
};
const postToDesktop = (type, payload) => {
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type, ...(payload || {}) }, "*");
return true;
}
return false;
};
const requestOnboardingReset = () => {
const message = [
"Reset onboarding and remove local 5etools data?",
@@ -89,20 +111,6 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
doToastInfo("Reset onboarding is unavailable in this context.");
};
window.addEventListener("message", (event) => {
const data = event?.data;
if (!data || typeof data !== "object") {
return;
}
if (data.type !== "5etd:reset-onboarding-result" || data.ok !== false) {
return;
}
const reason = typeof data.error === "string" && data.error ? data.error : "unknown error";
doToastInfo(\`Reset onboarding failed: \${reason}\`);
});
const closeDropdown = (li, toggle) => {
if (typeof NavBar !== "undefined" && typeof NavBar._closeDropdownElement === "function") {
NavBar._closeDropdownElement(li);
@@ -175,9 +183,21 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
menu.append(menuItem);
};
addMenuItem("New", () => doToastInfo("Characters: New is coming in a later build."));
addMenuItem("Load from JSON", () => doToastInfo("Characters: Load from JSON is coming in a later build."));
addMenuItem("My Characters", () => doToastInfo("Characters: My Characters is coming in a later build."));
addMenuItem("New", () => {
if (!postToDesktop("5etd:characters:new")) {
doToastInfo("Characters menu is unavailable in this context.");
}
});
addMenuItem("Load from JSON", () => {
if (!postToDesktop("5etd:characters:load-json")) {
doToastInfo("Characters menu is unavailable in this context.");
}
});
addMenuItem("My Characters", () => {
if (!postToDesktop("5etd:characters:list")) {
doToastInfo("Characters menu is unavailable in this context.");
}
});
li.append(toggle, menu);
@@ -329,9 +349,221 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string {
const applyDesktopMenuExtensions = () => {
addCharactersMenu();
addResetOnboardingMenu();
scheduleAutoFocusSearch();
bindNativeNavbarSync();
postNativeNavbarModel();
};
const NAVBAR_ACTION_ATTR = "data-5etd-nav-action-id";
const cleanNavLabel = (value) => String(value || "").replace(/\\s+/g, " ").replace(/[▾▼►]+/g, "").trim();
const getNavPrimaryText = (ele) => {
for (const node of Array.from(ele.childNodes || [])) {
if (node.nodeType === Node.TEXT_NODE) {
const text = cleanNavLabel(node.textContent || "");
if (text) return text;
}
}
return cleanNavLabel(ele.textContent || "");
};
const isActionLikeHref = (hrefRaw) => {
const href = String(hrefRaw || "").trim().toLowerCase();
return !href || href === "#" || href.startsWith("javascript:");
};
const parseNavbarMenuList = (list, idPrefix) => {
const entries = [];
const liElements = Array.from(list?.children || []).filter((it) => it instanceof HTMLLIElement);
liElements.forEach((li, index) => {
const entryId = \`\${idPrefix}-\${index}\`;
if (li.classList.contains("ve-dropdown-divider") || li.classList.contains("divider")) {
entries.push({ id: \`\${entryId}-divider\`, type: "divider" });
return;
}
if (li.classList.contains("nav2-list__label")) {
const label = cleanNavLabel(li.textContent || "");
if (label) entries.push({ id: \`\${entryId}-label\`, type: "label", label });
return;
}
if (li.classList.contains("dropdown--navbar")) {
const toggle = li.querySelector(":scope > a");
const submenuList = li.querySelector(":scope > ul");
const label = toggle ? getNavPrimaryText(toggle) : "";
if (!label) return;
entries.push({
id: \`\${entryId}-submenu\`,
type: "submenu",
label,
children: submenuList ? parseNavbarMenuList(submenuList, \`\${entryId}-submenu\`) : []
});
return;
}
const link = li.querySelector(":scope > a");
if (link) {
const label = getNavPrimaryText(link);
const href = String(link.getAttribute("href") || "").trim();
if (isActionLikeHref(href)) {
const actionId = \`\${entryId}-action\`;
link.setAttribute(NAVBAR_ACTION_ATTR, actionId);
entries.push({ id: \`\${entryId}-action\`, type: "action", label: label || "Action", actionId });
return;
}
entries.push({
id: \`\${entryId}-link\`,
type: "link",
label: label || href,
href,
isExternal: link.target === "_blank" || /^https?:\\/\\//i.test(href)
});
return;
}
const spans = Array.from(li.querySelectorAll(":scope > span")).filter((it) => it instanceof HTMLSpanElement);
spans.forEach((span, spanIndex) => {
const label = cleanNavLabel(span.textContent || span.title || "");
const actionId = \`\${entryId}-span-\${spanIndex}\`;
span.setAttribute(NAVBAR_ACTION_ATTR, actionId);
entries.push({ id: \`\${entryId}-span-\${spanIndex}\`, type: "action", label: label || "Action", actionId });
});
});
return entries;
};
const buildNativeNavbarGroups = () => {
const navbar = document.getElementById("navbar");
if (!navbar) return [];
const groups = [];
const rootItems = Array.from(navbar.children).filter((it) => it instanceof HTMLLIElement);
rootItems.forEach((li, index) => {
if (
li.classList.contains("omni__wrp-input") ||
li.classList.contains("desktop-nav-characters")
) {
return;
}
if (li.classList.contains("dropdown--navbar")) {
const toggle = li.querySelector(":scope > a");
const menuList = li.querySelector(":scope > ul");
const label = toggle ? getNavPrimaryText(toggle) : "";
if (!label) return;
groups.push({
id: \`root-\${index}-dropdown\`,
kind: "dropdown",
label,
items: menuList ? parseNavbarMenuList(menuList, \`root-\${index}-list\`) : []
});
return;
}
const link = li.querySelector(":scope > a");
if (!link) return;
const label = getNavPrimaryText(link);
const href = String(link.getAttribute("href") || "").trim();
if (!label) return;
if (isActionLikeHref(href)) {
const actionId = \`root-\${index}-action\`;
link.setAttribute(NAVBAR_ACTION_ATTR, actionId);
groups.push({
id: \`root-\${index}-action\`,
kind: "action",
label,
actionId
});
return;
}
groups.push({
id: \`root-\${index}-link\`,
kind: "link",
label,
href,
isExternal: link.target === "_blank" || /^https?:\\/\\//i.test(href)
});
});
return groups;
};
const postNativeNavbarModel = () => {
if (!window.parent || window.parent === window) return;
window.parent.postMessage(
{
type: "5etd:native-navbar-data",
groups: buildNativeNavbarGroups()
},
"*"
);
};
let navbarSyncObserver = null;
const bindNativeNavbarSync = () => {
const navbar = document.getElementById("navbar");
if (!navbar || navbar.dataset.desktopNavSyncBound === "1") {
return;
}
navbar.dataset.desktopNavSyncBound = "1";
navbarSyncObserver?.disconnect?.();
navbarSyncObserver = new MutationObserver(() => {
window.requestAnimationFrame(postNativeNavbarModel);
});
navbarSyncObserver.observe(navbar, { childList: true, subtree: true, attributes: true, characterData: true });
};
const runNativeNavbarAction = (actionId) => {
const id = String(actionId || "").trim();
if (!id) return;
const navbar = document.getElementById("navbar");
if (!navbar) return;
const actionEle = navbar.querySelector(\`[\${NAVBAR_ACTION_ATTR}="\${id}"]\`);
if (!(actionEle instanceof HTMLElement)) {
return;
}
actionEle.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
actionEle.click();
};
window.addEventListener("message", (event) => {
const data = event?.data;
if (!data || typeof data !== "object") {
return;
}
if (data.type === "5etd:reset-onboarding-result" && data.ok === false) {
const reason = typeof data.error === "string" && data.error ? data.error : "unknown error";
doToastInfo(\`Reset onboarding failed: \${reason}\`);
return;
}
if (data.type === "5etd:native-navbar-request") {
bindNativeNavbarSync();
postNativeNavbarModel();
return;
}
if (data.type === "5etd:native-navbar-run-action") {
runNativeNavbarAction(data.actionId);
}
});
window.addEventListener("DOMContentLoaded", applyDesktopMenuExtensions);
window.addEventListener("load", applyDesktopMenuExtensions);
})();
@@ -1001,6 +1233,27 @@ html.desktop-reskin .modal__inner {
border-radius: 12px !important;
box-shadow: 0 16px 28px rgba(0, 0, 0, 0.35);
}
/* App-managed top bar mode: keep 5etools content, hide native header/nav chrome */
html.desktop-reskin.desktop-app-navbar-mode body {
padding-top: 0 !important;
}
html.desktop-reskin.desktop-app-navbar-mode .viewport-wrapper {
inset: 0 !important;
height: auto !important;
min-height: 100vh !important;
}
html.desktop-reskin.desktop-app-navbar-mode header.page__header,
html.desktop-reskin.desktop-app-navbar-mode .page__header,
html.desktop-reskin.desktop-app-navbar-mode #navigation,
html.desktop-reskin.desktop-app-navbar-mode nav.page__nav,
html.desktop-reskin.desktop-app-navbar-mode #navigation.page__nav,
html.desktop-reskin.desktop-app-navbar-mode .page__nav,
html.desktop-reskin.desktop-app-navbar-mode .page__disp-download-progress {
display: none !important;
}
`;
export interface WikiServer {
@@ -1127,10 +1380,10 @@ async function pTryServeExternalImage(
const responseBuffer = Buffer.from(await response.arrayBuffer());
const contentType = response.headers.get("content-type") ?? getMimeType(externalImageUrl.pathname);
res.writeHead(200, {
res.writeHead(200, withCorsHeaders({
"Content-Type": contentType,
"Cache-Control": "public, max-age=3600"
});
}));
res.end(responseBuffer);
return true;
} catch {
@@ -1184,13 +1437,19 @@ export async function startWikiServer(options: StartWikiServerOptions = {}): Pro
return await new Promise<WikiServer>((resolve, reject) => {
const server = http.createServer((req, res) => {
if (req.method === "OPTIONS") {
res.writeHead(204, withCorsHeaders({ "Content-Type": "text/plain; charset=utf-8" }));
res.end();
return;
}
const url = req.url ?? "/";
let filePath: string;
try {
filePath = resolveRequestPath(rootDir, url);
} catch {
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
res.writeHead(403, withCorsHeaders({ "Content-Type": "text/plain; charset=utf-8" }));
res.end("Forbidden");
return;
}
@@ -1203,13 +1462,13 @@ export async function startWikiServer(options: StartWikiServerOptions = {}): Pro
return;
}
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.writeHead(404, withCorsHeaders({ "Content-Type": "text/plain; charset=utf-8" }));
res.end("Not Found");
});
return;
}
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.writeHead(500, withCorsHeaders({ "Content-Type": "text/plain; charset=utf-8" }));
res.end("Internal Server Error");
return;
}
@@ -1218,10 +1477,10 @@ export async function startWikiServer(options: StartWikiServerOptions = {}): Pro
const transformedBody =
extension === ".html" && SHOULD_INJECT_RESKIN ? injectDesktopReskin(data.toString("utf8"), reskinBootstrap) : data;
res.writeHead(200, {
res.writeHead(200, withCorsHeaders({
"Content-Type": getMimeType(filePath),
"Cache-Control": "no-store"
});
}));
if (typeof transformedBody === "string") {
res.end(transformedBody, "utf8");