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:
1 parent
8be4c8d571
commit
a73c5ea002
10 files changed
+10331
-195
No files matched your search
+285
-26
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user