From 18734acaadc1a6d347181a17b40bdaedf50118b4 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 26 Feb 2026 01:13:22 +0000 Subject: [PATCH] added nested navmenus --- src/main/characterData.ts | 160 +++++++++++++++++++------ src/main/wikiServer.ts | 100 +++++++++++++++- src/renderer/App.tsx | 243 ++++++++++++++++++++++++++++++++------ src/renderer/styles.css | 107 +++++++++++++++-- 4 files changed, 525 insertions(+), 85 deletions(-) diff --git a/src/main/characterData.ts b/src/main/characterData.ts index 38305af..a4dcabf 100644 --- a/src/main/characterData.ts +++ b/src/main/characterData.ts @@ -54,6 +54,11 @@ interface SourceBookRecord { published?: string; } +interface GeneratedNavSourceRecord extends SourceBookRecord { + parentSource?: string; + parentName?: string; +} + interface ClassRecord { name?: string; source?: string; @@ -130,6 +135,15 @@ interface CacheValue { rootDir: string; options: CharacterSelectionOptionSets; detailsByRef: Map; + rawLocators: Map; +} + +type RawEntityArrayKey = "class" | "background" | "race"; + +interface RawEntityLocator { + filePath: string; + arrayKey: RawEntityArrayKey; + index: number; } let cache: CacheValue | null = null; @@ -436,24 +450,25 @@ function classifySourceMeta(input: { function buildSourceMetaMap(rootDir: string): Map { const rawMap = new Map(); + const addRawSourceRecord = (record: SourceBookRecord): void => { + const source = normalizeString(record.source).toUpperCase(); + if (!source || rawMap.has(source)) { + return; + } + + rawMap.set(source, { + sourceName: normalizeString(record.name) || source, + sourceGroup: normalizeString(record.group) || null, + sourcePublished: normalizeString(record.published) || null + }); + }; const booksPath = path.join(rootDir, "data", "books.json"); if (fs.existsSync(booksPath)) { const parsed = readJsonFile(booksPath); const books = isRecord(parsed) && Array.isArray(parsed.book) ? (parsed.book as SourceBookRecord[]) : []; for (const book of books) { - const source = normalizeString(book.source).toUpperCase(); - if (!source) { - continue; - } - - if (!rawMap.has(source)) { - rawMap.set(source, { - sourceName: normalizeString(book.name) || source, - sourceGroup: normalizeString(book.group) || null, - sourcePublished: normalizeString(book.published) || null - }); - } + addRawSourceRecord(book); } } @@ -465,16 +480,22 @@ function buildSourceMetaMap(rootDir: string): Map { : []; for (const adventure of adventures) { - const source = normalizeString(adventure.source).toUpperCase(); - if (!source || rawMap.has(source)) { - continue; - } + addRawSourceRecord(adventure); + } + } - rawMap.set(source, { - sourceName: normalizeString(adventure.name) || source, - sourceGroup: normalizeString(adventure.group) || null, - sourcePublished: normalizeString(adventure.published) || null - }); + const generatedNavIndexPath = path.join(rootDir, "data", "generated", "gendata-nav-adventure-book-index.json"); + if (fs.existsSync(generatedNavIndexPath)) { + const parsed = readJsonFile(generatedNavIndexPath); + const books = isRecord(parsed) && Array.isArray(parsed.book) + ? (parsed.book as GeneratedNavSourceRecord[]) + : []; + const adventures = isRecord(parsed) && Array.isArray(parsed.adventure) + ? (parsed.adventure as GeneratedNavSourceRecord[]) + : []; + + for (const record of [...books, ...adventures]) { + addRawSourceRecord(record); } } @@ -814,8 +835,32 @@ function createOption( }; } +function readRawJsonFromLocator(locator: RawEntityLocator): string { + try { + if (!fs.existsSync(locator.filePath)) { + return ""; + } + + const parsed = readJsonFile(locator.filePath); + const arr = isRecord(parsed) ? parsed[locator.arrayKey] : null; + if (!Array.isArray(arr)) { + return ""; + } + + const entry = arr[locator.index]; + if (entry == null) { + return ""; + } + + return JSON.stringify(entry, null, 2); + } catch { + return ""; + } +} + function buildCache(rootDir: string): CacheValue { const detailsByRef = new Map(); + const rawLocators = new Map(); const sourceMetaMap = buildSourceMetaMap(rootDir); const classFluffMap = loadClassFluff(rootDir); @@ -827,7 +872,7 @@ function buildCache(rootDir: string): CacheValue { const classIndexJson = readJsonFile(classIndexPath); const classFileNames = isRecord(classIndexJson) ? Object.values(classIndexJson).filter((it): it is string => typeof it === "string") : []; - const allClasses: ClassRecord[] = []; + const allClasses: Array<{ record: ClassRecord; filePath: string; index: number }> = []; const allClassFeatures: ClassFeatureRecord[] = []; const allSubclasses: SubclassRecord[] = []; @@ -843,7 +888,10 @@ function buildCache(rootDir: string): CacheValue { } if (Array.isArray(parsed.class)) { - allClasses.push(...(parsed.class as ClassRecord[])); + const classRecords = parsed.class as ClassRecord[]; + classRecords.forEach((record, classIndex) => { + allClasses.push({ record, filePath, index: classIndex }); + }); } if (Array.isArray(parsed.classFeature)) { @@ -871,7 +919,8 @@ function buildCache(rootDir: string): CacheValue { } const classOptions: CharacterSelectionOption[] = []; - for (const cls of allClasses) { + for (const clsInfo of allClasses) { + const cls = clsInfo.record; const name = normalizeString(cls.name); const source = normalizeString(cls.source).toUpperCase(); if (!name || !source) { @@ -994,11 +1043,16 @@ function buildCache(rootDir: string): CacheValue { const detail: CharacterSelectionDetail = { ...option, featureCards, - rawJson: JSON.stringify(cls, null, 2) + rawJson: "" }; classOptions.push(option); detailsByRef.set(option.entityRef, detail); + rawLocators.set(option.entityRef, { + filePath: clsInfo.filePath, + arrayKey: "class", + index: clsInfo.index + }); } const backgroundPath = path.join(rootDir, "data", "backgrounds.json"); @@ -1008,7 +1062,8 @@ function buildCache(rootDir: string): CacheValue { : []; const backgroundOptions: CharacterSelectionOption[] = []; - for (const bg of backgroundRecords) { + for (let bgIndex = 0; bgIndex < backgroundRecords.length; bgIndex += 1) { + const bg = backgroundRecords[bgIndex]; const name = normalizeString(bg.name); const source = normalizeString(bg.source).toUpperCase(); if (!name || !source) { @@ -1035,11 +1090,16 @@ function buildCache(rootDir: string): CacheValue { const detail: CharacterSelectionDetail = { ...option, featureCards, - rawJson: JSON.stringify(bg, null, 2) + rawJson: "" }; backgroundOptions.push(option); detailsByRef.set(option.entityRef, detail); + rawLocators.set(option.entityRef, { + filePath: backgroundPath, + arrayKey: "background", + index: bgIndex + }); } const speciesPath = path.join(rootDir, "data", "races.json"); @@ -1049,7 +1109,8 @@ function buildCache(rootDir: string): CacheValue { : []; const speciesOptions: CharacterSelectionOption[] = []; - for (const race of speciesRecords) { + for (let raceIndex = 0; raceIndex < speciesRecords.length; raceIndex += 1) { + const race = speciesRecords[raceIndex]; const name = normalizeString(race.name); const source = normalizeString(race.source).toUpperCase(); if (!name || !source) { @@ -1078,11 +1139,16 @@ function buildCache(rootDir: string): CacheValue { const detail: CharacterSelectionDetail = { ...option, featureCards, - rawJson: JSON.stringify(race, null, 2) + rawJson: "" }; speciesOptions.push(option); detailsByRef.set(option.entityRef, detail); + rawLocators.set(option.entityRef, { + filePath: speciesPath, + arrayKey: "race", + index: raceIndex + }); } const sortOptions = (arr: CharacterSelectionOption[]): CharacterSelectionOption[] => @@ -1097,16 +1163,18 @@ function buildCache(rootDir: string): CacheValue { backgrounds: sortOptions(backgroundOptions), species: sortOptions(speciesOptions) }, - detailsByRef + detailsByRef, + rawLocators }; } function getCache(rootDir: string): CacheValue { - if (cache && cache.rootDir === rootDir) { + const resolvedRootDir = path.resolve(rootDir); + if (cache && cache.rootDir === resolvedRootDir) { return cache; } - const value = buildCache(rootDir); + const value = buildCache(resolvedRootDir); cache = value; return value; } @@ -1121,8 +1189,32 @@ export function getCharacterSelectionDetail(rootDir: string, entityRefRaw: strin return null; } - const detail = getCache(rootDir).detailsByRef.get(entityRef); - return detail || null; + const cacheValue = getCache(rootDir); + const detail = cacheValue.detailsByRef.get(entityRef); + if (!detail) { + return null; + } + + if (detail.rawJson) { + return detail; + } + + const locator = cacheValue.rawLocators.get(entityRef); + if (!locator) { + return detail; + } + + const nextRawJson = readRawJsonFromLocator(locator); + if (!nextRawJson) { + return detail; + } + + const nextDetail: CharacterSelectionDetail = { + ...detail, + rawJson: nextRawJson + }; + cacheValue.detailsByRef.set(entityRef, nextDetail); + return nextDetail; } export function clearCharacterSelectionCache(): void { diff --git a/src/main/wikiServer.ts b/src/main/wikiServer.ts index 75604dd..d4bda41 100644 --- a/src/main/wikiServer.ts +++ b/src/main/wikiServer.ts @@ -367,6 +367,32 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { return cleanNavLabel(ele.textContent || ""); }; + const getNavEntryMeta = (ele) => { + if (!ele) return {}; + + const out = {}; + const dateEle = ele.querySelector(":scope > .page__nav-date"); + if (dateEle) { + const datePrefix = cleanNavLabel(dateEle.textContent || ""); + if (datePrefix) out.datePrefix = datePrefix; + } + + const sourceEle = ele.querySelector(":scope > .nav2-list__disp-source"); + if (sourceEle instanceof HTMLElement) { + const computed = getComputedStyle(sourceEle); + const sourceColor = String(computed.backgroundColor || computed.borderLeftColor || "").trim(); + if ( + sourceColor && + sourceColor !== "transparent" && + !/^rgba?\\(\\s*0\\s*,\\s*0\\s*,\\s*0(?:\\s*,\\s*0)?\\s*\\)$/i.test(sourceColor) + ) { + out.sourceColor = sourceColor; + } + } + + return out; + }; + const isActionLikeHref = (hrefRaw) => { const href = String(hrefRaw || "").trim().toLowerCase(); return !href || href === "#" || href.startsWith("javascript:"); @@ -378,6 +404,9 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { liElements.forEach((li, index) => { const entryId = \`\${idPrefix}-\${index}\`; + const submenuList = li.querySelector(":scope > ul") + || (li.classList.contains("nav2-accord__wrp") ? li.querySelector(":scope > .nav2-accord__body") : null); + const submenuChildren = submenuList ? parseNavbarMenuList(submenuList, \`\${entryId}-submenu\`) : []; if (li.classList.contains("ve-dropdown-divider") || li.classList.contains("divider")) { entries.push({ id: \`\${entryId}-divider\`, type: "divider" }); @@ -392,7 +421,6 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { 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; @@ -400,7 +428,23 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { id: \`\${entryId}-submenu\`, type: "submenu", label, - children: submenuList ? parseNavbarMenuList(submenuList, \`\${entryId}-submenu\`) : [] + ...getNavEntryMeta(toggle), + children: submenuChildren + }); + return; + } + + if (li.classList.contains("nav2-accord__wrp")) { + const headPrimary = li.querySelector(":scope > .nav2-accord__head > :first-child"); + const label = headPrimary ? getNavPrimaryText(headPrimary) : ""; + if (!label || !submenuChildren.length) return; + + entries.push({ + id: \`\${entryId}-accordion\`, + type: "submenu", + label, + ...getNavEntryMeta(headPrimary), + children: submenuChildren }); return; } @@ -409,6 +453,33 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { if (link) { const label = getNavPrimaryText(link); const href = String(link.getAttribute("href") || "").trim(); + if (submenuChildren.length) { + if (isActionLikeHref(href)) { + const actionId = \`\${entryId}-submenu-action\`; + link.setAttribute(NAVBAR_ACTION_ATTR, actionId); + entries.push({ + id: \`\${entryId}-submenu-link\`, + type: "submenu", + label: label || "Section", + ...getNavEntryMeta(link), + actionId, + children: submenuChildren + }); + return; + } + + entries.push({ + id: \`\${entryId}-submenu-link\`, + type: "submenu", + label: label || href, + ...getNavEntryMeta(link), + href, + isExternal: link.target === "_blank" || /^https?:\\/\\//i.test(href), + children: submenuChildren + }); + return; + } + if (isActionLikeHref(href)) { const actionId = \`\${entryId}-action\`; link.setAttribute(NAVBAR_ACTION_ATTR, actionId); @@ -420,6 +491,7 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { id: \`\${entryId}-link\`, type: "link", label: label || href, + ...getNavEntryMeta(link), href, isExternal: link.target === "_blank" || /^https?:\\/\\//i.test(href) }); @@ -427,11 +499,33 @@ function getDesktopReskinBootstrap(externalImageRoot: string | null): string { } const spans = Array.from(li.querySelectorAll(":scope > span")).filter((it) => it instanceof HTMLSpanElement); + if (submenuChildren.length && spans.length) { + const first = spans[0]; + const label = cleanNavLabel(first.textContent || first.title || ""); + const actionId = \`\${entryId}-submenu-span\`; + first.setAttribute(NAVBAR_ACTION_ATTR, actionId); + entries.push({ + id: \`\${entryId}-submenu-span\`, + type: "submenu", + label: label || "Section", + ...getNavEntryMeta(first), + actionId, + children: submenuChildren + }); + return; + } + 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 }); + entries.push({ + id: \`\${entryId}-span-\${spanIndex}\`, + type: "action", + label: label || "Action", + ...getNavEntryMeta(span), + actionId + }); }); }); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a72b6c1..73ba51d 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -107,6 +107,8 @@ type TopbarMenuEntry = { id: string; type: "link" | "action" | "divider" | "label" | "submenu"; label?: string; + datePrefix?: string; + sourceColor?: string; href?: string; isExternal?: boolean; actionId?: string; @@ -161,6 +163,37 @@ function getElementPrimaryText(ele: Element): string { return cleanTopbarLabel(ele.textContent || ""); } +function getElementMenuMeta(ele: Element | null): Pick { + if (!ele) { + return {}; + } + + const out: Pick = {}; + + const dateEle = ele.querySelector(":scope > .page__nav-date"); + if (dateEle) { + const datePrefix = cleanTopbarLabel(dateEle.textContent || ""); + if (datePrefix) { + out.datePrefix = datePrefix; + } + } + + const sourceEle = ele.querySelector(":scope > .nav2-list__disp-source"); + if (sourceEle instanceof HTMLElement) { + const computed = getComputedStyle(sourceEle); + const sourceColor = (computed.backgroundColor || computed.borderLeftColor || "").trim(); + if ( + sourceColor + && sourceColor !== "transparent" + && !/^rgba?\(\s*0\s*,\s*0\s*,\s*0(?:\s*,\s*0)?\s*\)$/i.test(sourceColor) + ) { + out.sourceColor = sourceColor; + } + } + + return out; +} + function getPathnameFromHrefLoose(hrefRaw: string): string { const href = hrefRaw.trim(); if (!href) { @@ -178,7 +211,7 @@ function areTopbarGroupsEquivalent(a: TopbarMenuGroup[], b: TopbarMenuGroup[]): } function parseNativeTopbarMenuList( - list: HTMLUListElement, + list: Element, registerAction: (ele: HTMLElement) => string, idPrefix: string ): TopbarMenuEntry[] { @@ -187,6 +220,11 @@ function parseNativeTopbarMenuList( liElements.forEach((li, index) => { const entryId = `${idPrefix}-${index}`; + const submenuList = ( + li.querySelector(":scope > ul") + || (li.classList.contains("nav2-accord__wrp") ? li.querySelector(":scope > .nav2-accord__body") : null) + ) as Element | null; + const submenuChildren = submenuList ? parseNativeTopbarMenuList(submenuList, registerAction, `${entryId}-submenu`) : []; if (li.classList.contains("ve-dropdown-divider") || li.classList.contains("divider")) { entries.push({ id: `${entryId}-divider`, type: "divider" }); return; @@ -202,7 +240,6 @@ function parseNativeTopbarMenuList( if (li.classList.contains("dropdown--navbar")) { const toggle = li.querySelector(":scope > a") as HTMLAnchorElement | null; - const submenuList = li.querySelector(":scope > ul") as HTMLUListElement | null; const label = toggle ? getElementPrimaryText(toggle) : ""; if (!label) { return; @@ -211,7 +248,24 @@ function parseNativeTopbarMenuList( id: `${entryId}-submenu`, type: "submenu", label, - children: submenuList ? parseNativeTopbarMenuList(submenuList, registerAction, `${entryId}-submenu`) : [] + ...getElementMenuMeta(toggle), + children: submenuChildren + }); + return; + } + + if (li.classList.contains("nav2-accord__wrp")) { + const headPrimary = li.querySelector(":scope > .nav2-accord__head > :first-child"); + const label = headPrimary ? getElementPrimaryText(headPrimary) : ""; + if (!label || !submenuChildren.length) { + return; + } + entries.push({ + id: `${entryId}-accordion`, + type: "submenu", + label, + ...getElementMenuMeta(headPrimary), + children: submenuChildren }); return; } @@ -221,11 +275,25 @@ function parseNativeTopbarMenuList( const label = getElementPrimaryText(link); const href = (link.getAttribute("href") || "").trim(); const isActionLike = href === "#" || href.toLowerCase().startsWith("javascript:"); + if (submenuChildren.length) { + entries.push({ + id: `${entryId}-submenu-link`, + type: "submenu", + label: label || "Section", + ...getElementMenuMeta(link), + href: !isActionLike ? href : undefined, + actionId: isActionLike ? registerAction(link) : undefined, + isExternal: !isActionLike ? link.target === "_blank" || /^https?:\/\//i.test(href) : false, + children: submenuChildren + }); + return; + } if (isActionLike) { entries.push({ id: `${entryId}-action`, type: "action", label: label || "Action", + ...getElementMenuMeta(link), actionId: registerAction(link) }); return; @@ -235,6 +303,7 @@ function parseNativeTopbarMenuList( id: `${entryId}-link`, type: "link", label: label || href, + ...getElementMenuMeta(link), href, isExternal: link.target === "_blank" || /^https?:\/\//i.test(href) }); @@ -242,12 +311,26 @@ function parseNativeTopbarMenuList( } const spans = Array.from(li.querySelectorAll(":scope > span")).filter((it): it is HTMLSpanElement => it instanceof HTMLSpanElement); + if (submenuChildren.length && spans.length) { + const first = spans[0]; + const label = cleanTopbarLabel(first.textContent || first.title || ""); + entries.push({ + id: `${entryId}-submenu-span`, + type: "submenu", + label: label || "Section", + ...getElementMenuMeta(first), + actionId: registerAction(first), + children: submenuChildren + }); + return; + } spans.forEach((span, spanIndex) => { const label = cleanTopbarLabel(span.textContent || span.title || ""); entries.push({ id: `${entryId}-span-${spanIndex}`, type: "action", label: label || "Action", + ...getElementMenuMeta(span), actionId: registerAction(span) }); }); @@ -1913,6 +1996,7 @@ function WikiView() { const [currentPath, setCurrentPath] = useState(DEFAULT_PAGE); const [isRenderRuntimeReady, setIsRenderRuntimeReady] = useState(false); const [openTopMenuId, setOpenTopMenuId] = useState(null); + const [openTopMenuPath, setOpenTopMenuPath] = useState([]); const [topbarSearch, setTopbarSearch] = useState(""); const [topbarGroups, setTopbarGroups] = useState([]); const frameRef = useRef(null); @@ -2493,6 +2577,7 @@ function WikiView() { setCurrentPath(path); setCharacterModalMode("closed"); setOpenTopMenuId(null); + setOpenTopMenuPath([]); }, []); const onSubmitTopbarSearch = useCallback((event: FormEvent) => { @@ -2555,6 +2640,7 @@ function WikiView() { return; } frameRef.current?.contentWindow?.postMessage({ type: "5etd:native-navbar-run-action", actionId }, "*"); + setOpenTopMenuPath([]); }, []); const navigateTopbarHref = useCallback((hrefRaw: string, isExternal?: boolean) => { @@ -2583,13 +2669,13 @@ function WikiView() { }, [navigateWikiPath, resolveWikiPathFromHref]); const activateTopbarEntry = useCallback((entry: TopbarMenuEntry) => { - if (entry.type === "action") { + if (entry.actionId) { runTopbarAction(entry.actionId); setOpenTopMenuId(null); return; } - if (entry.type === "link" && entry.href) { + if (entry.href) { navigateTopbarHref(entry.href, entry.isExternal); return; } @@ -2598,6 +2684,7 @@ function WikiView() { const activateTopbarGroup = useCallback((group: TopbarMenuGroup) => { if (group.kind === "dropdown") { setOpenTopMenuId((current) => (current === group.id ? null : group.id)); + setOpenTopMenuPath([]); return; } @@ -2612,39 +2699,118 @@ function WikiView() { } }, [navigateTopbarHref, runTopbarAction]); - const renderTopbarEntries = useCallback((entries: TopbarMenuEntry[], depth = 0): JSX.Element[] => { - return entries.flatMap((entry) => { - if (entry.type === "divider") { - return [
]; - } + useEffect(() => { + if (!openTopMenuId) { + setOpenTopMenuPath([]); + } + }, [openTopMenuId]); - if (entry.type === "label") { - return [
{entry.label}
]; + const openTopbarSubmenuAtDepth = useCallback((depth: number, entryId: string) => { + setOpenTopMenuPath((current) => { + if (current[depth] === entryId && current.length === depth + 1) { + return current; } - - if (entry.type === "submenu") { - const children = entry.children?.length ? renderTopbarEntries(entry.children, depth + 1) : []; - return [ -
0 ? "topbar-submenu topbar-submenu--nested" : "topbar-submenu"}> -
{entry.label}
-
{children}
-
- ]; - } - - const isActive = entry.type === "link" && entry.href ? isSameAppPath(entry.href, currentPathname) : false; - return [ - - ]; + return [...current.slice(0, depth), entryId]; }); - }, [activateTopbarEntry, currentPathname, isSameAppPath]); + }, []); + + const renderTopbarDropdown = useCallback((entries: TopbarMenuEntry[]): JSX.Element => { + const renderEntryLabel = (entry: TopbarMenuEntry): JSX.Element => ( + + {entry.datePrefix ? {entry.datePrefix} : null} + {entry.sourceColor ? + ); + + const columns: TopbarMenuEntry[][] = [entries]; + let currentEntries = entries; + + for (let depth = 0; depth < openTopMenuPath.length; depth += 1) { + const activeSubmenuId = openTopMenuPath[depth]; + if (!activeSubmenuId) { + break; + } + const submenuEntry = currentEntries.find((entry) => entry.type === "submenu" && entry.id === activeSubmenuId); + if (!submenuEntry?.children?.length) { + break; + } + columns.push(submenuEntry.children); + currentEntries = submenuEntry.children; + } + + return ( +
+ {columns.map((columnEntries, depth) => ( +
+ {columnEntries.map((entry) => { + if (entry.type === "divider") { + return
; + } + + if (entry.type === "label") { + return
{entry.label}
; + } + + if (entry.type === "submenu") { + const isSubmenuActive = openTopMenuPath[depth] === entry.id; + const hasChildren = Boolean(entry.children?.length); + const className = [ + "topbar-dropdown-item", + "topbar-dropdown-item--submenu", + isSubmenuActive ? "is-active" : "" + ] + .filter(Boolean) + .join(" "); + + return ( + + ); + } + + const isActive = entry.type === "link" && entry.href ? isSameAppPath(entry.href, currentPathname) : false; + return ( + + ); + })} +
+ ))} +
+ ); + }, [activateTopbarEntry, currentPathname, isSameAppPath, openTopMenuPath, openTopbarSubmenuAtDepth]); const sourceBuckets = characterDraft?.steps.preferences.sourceBuckets ?? []; @@ -3649,7 +3815,7 @@ function WikiView() { {group.kind === "dropdown" && isOpen ? (
- {renderTopbarEntries(group.items ?? [])} + {renderTopbarDropdown(group.items ?? [])}
) : null}
@@ -3660,7 +3826,10 @@ function WikiView() {