web-mojo 2.2.65 → 2.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +1 -1
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunks/ChatView-Cfe0ZGvr.js +2 -0
- package/dist/chunks/ChatView-Cfe0ZGvr.js.map +1 -0
- package/dist/chunks/ChatView-DuQVFrCY.js +2 -0
- package/dist/chunks/ChatView-DuQVFrCY.js.map +1 -0
- package/dist/chunks/{Collection-1sPoIFvQ.js → Collection-BWKmydl5.js} +2 -2
- package/dist/chunks/{Collection-1sPoIFvQ.js.map → Collection-BWKmydl5.js.map} +1 -1
- package/dist/chunks/{Collection-DSBRXpwK.js → Collection-CmjTsmrP.js} +2 -2
- package/dist/chunks/{Collection-DSBRXpwK.js.map → Collection-CmjTsmrP.js.map} +1 -1
- package/dist/chunks/ContextMenu-8vTiZZQV.js +2 -0
- package/dist/chunks/ContextMenu-8vTiZZQV.js.map +1 -0
- package/dist/chunks/ContextMenu-DBw0WMTO.js +2 -0
- package/dist/chunks/ContextMenu-DBw0WMTO.js.map +1 -0
- package/dist/chunks/{DataView--nUWtq6r.js → DataView-BEovBggn.js} +2 -2
- package/dist/chunks/{DataView--nUWtq6r.js.map → DataView-BEovBggn.js.map} +1 -1
- package/dist/chunks/{DataView-CK3Z0TJH.js → DataView-DyJKgOn3.js} +2 -2
- package/dist/chunks/{DataView-CK3Z0TJH.js.map → DataView-DyJKgOn3.js.map} +1 -1
- package/dist/chunks/{Dialog-VoLlToMl.js → Dialog-DW7PHzUc.js} +2 -2
- package/dist/chunks/{Dialog-wBhTkeWg.js.map → Dialog-DW7PHzUc.js.map} +1 -1
- package/dist/chunks/{Dialog-wBhTkeWg.js → Dialog-jfBsXy5X.js} +2 -2
- package/dist/chunks/{Dialog-VoLlToMl.js.map → Dialog-jfBsXy5X.js.map} +1 -1
- package/dist/chunks/Files-C-ChBvr5.js +2 -0
- package/dist/chunks/Files-C-ChBvr5.js.map +1 -0
- package/dist/chunks/Files-DNbHDy43.js +2 -0
- package/dist/chunks/Files-DNbHDy43.js.map +1 -0
- package/dist/chunks/FormView-EoB_ZdIB.js +3 -0
- package/dist/chunks/FormView-EoB_ZdIB.js.map +1 -0
- package/dist/chunks/FormView-Q_lFA0nr.js +3 -0
- package/dist/chunks/FormView-Q_lFA0nr.js.map +1 -0
- package/dist/chunks/{ListView-6JQ6tRXs.js → ListView-BLFFK_Ir.js} +2 -2
- package/dist/chunks/{ListView-6JQ6tRXs.js.map → ListView-BLFFK_Ir.js.map} +1 -1
- package/dist/chunks/{ListView-DVStKiMi.js → ListView-CMZpwyyC.js} +2 -2
- package/dist/chunks/{ListView-DVStKiMi.js.map → ListView-CMZpwyyC.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-CnAEbUw_.js → MetricsCountryMapView-B0kWK-Js.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-CnAEbUw_.js.map → MetricsCountryMapView-B0kWK-Js.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-J067qrrt.js → MetricsCountryMapView-DuBKO7gz.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-J067qrrt.js.map → MetricsCountryMapView-DuBKO7gz.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-DtWq2_YJ.js → MetricsMiniChartWidget-BkMjI-gz.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-DtWq2_YJ.js.map → MetricsMiniChartWidget-BkMjI-gz.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-CtCN1rtt.js → MetricsMiniChartWidget-ChC5GGm6.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-CtCN1rtt.js.map → MetricsMiniChartWidget-ChC5GGm6.js.map} +1 -1
- package/dist/chunks/{PDFViewer-BNCyDElg.js → PDFViewer-iOqYpg-6.js} +2 -2
- package/dist/chunks/{PDFViewer-BNCyDElg.js.map → PDFViewer-iOqYpg-6.js.map} +1 -1
- package/dist/chunks/{PDFViewer-Di4OHAkv.js → PDFViewer-sFoyopz3.js} +2 -2
- package/dist/chunks/{PDFViewer-Di4OHAkv.js.map → PDFViewer-sFoyopz3.js.map} +1 -1
- package/dist/chunks/{Rest-Ds9e8tN8.js → Rest-B1eUyLX5.js} +2 -2
- package/dist/chunks/{Rest-Ds9e8tN8.js.map → Rest-B1eUyLX5.js.map} +1 -1
- package/dist/chunks/{Rest-DHbszkuP.js → Rest-BJ3Mvx1L.js} +2 -2
- package/dist/chunks/{Rest-DHbszkuP.js.map → Rest-BJ3Mvx1L.js.map} +1 -1
- package/dist/chunks/TokenManager-BYMKH_aW.js +2 -0
- package/dist/chunks/{TokenManager-D-9tqubS.js.map → TokenManager-BYMKH_aW.js.map} +1 -1
- package/dist/chunks/TokenManager-DhDUKmaw.js +2 -0
- package/dist/chunks/{TokenManager-8JM2qj_1.js.map → TokenManager-DhDUKmaw.js.map} +1 -1
- package/dist/chunks/User-BnlvMG5J.js +3 -0
- package/dist/chunks/User-BnlvMG5J.js.map +1 -0
- package/dist/chunks/User-DSqcOwPL.js +3 -0
- package/dist/chunks/User-DSqcOwPL.js.map +1 -0
- package/dist/chunks/{WebApp-JJAK0eNM.js → WebApp-B0m6JCjO.js} +2 -2
- package/dist/chunks/{WebApp-JJAK0eNM.js.map → WebApp-B0m6JCjO.js.map} +1 -1
- package/dist/chunks/{WebApp-BdJA4Uup.js → WebApp-Bsic6FPo.js} +2 -2
- package/dist/chunks/{WebApp-BdJA4Uup.js.map → WebApp-Bsic6FPo.js.map} +1 -1
- package/dist/chunks/{WebSocketClient-BoF7TV7i.js → WebSocketClient-Bh0Mmtje.js} +2 -2
- package/dist/chunks/{WebSocketClient-BoF7TV7i.js.map → WebSocketClient-Bh0Mmtje.js.map} +1 -1
- package/dist/chunks/{WebSocketClient-BxL2M7p0.js → WebSocketClient-CLgYPxWX.js} +2 -2
- package/dist/chunks/{WebSocketClient-BxL2M7p0.js.map → WebSocketClient-CLgYPxWX.js.map} +1 -1
- package/dist/chunks/{version-kTtMlfsq.js → version-BY7AsEkb.js} +2 -2
- package/dist/chunks/{version-kTtMlfsq.js.map → version-BY7AsEkb.js.map} +1 -1
- package/dist/chunks/{version-Cn26llLs.js → version-BdfRyQDm.js} +2 -2
- package/dist/chunks/{version-Cn26llLs.js.map → version-BdfRyQDm.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.cjs.js.map +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.cjs.js.map +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/lightbox.es.js.map +1 -1
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -1
- package/dist/web-mojo.lite.iife.js +1230 -2
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +95 -74
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +8 -7
- package/dist/chunks/ChatView-D1-ev2qA.js +0 -2
- package/dist/chunks/ChatView-D1-ev2qA.js.map +0 -1
- package/dist/chunks/ChatView-DesGp57e.js +0 -2
- package/dist/chunks/ChatView-DesGp57e.js.map +0 -1
- package/dist/chunks/ContextMenu-CsQGpSrv.js +0 -3
- package/dist/chunks/ContextMenu-CsQGpSrv.js.map +0 -1
- package/dist/chunks/ContextMenu-dqFxitXY.js +0 -3
- package/dist/chunks/ContextMenu-dqFxitXY.js.map +0 -1
- package/dist/chunks/FormView-BQnvMheR.js +0 -3
- package/dist/chunks/FormView-BQnvMheR.js.map +0 -1
- package/dist/chunks/FormView-C7FnbmEI.js +0 -3
- package/dist/chunks/FormView-C7FnbmEI.js.map +0 -1
- package/dist/chunks/TokenManager-8JM2qj_1.js +0 -2
- package/dist/chunks/TokenManager-D-9tqubS.js +0 -2
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/core/utils/ConsoleSilencer.js","../src/core/views/navigation/GroupSearchView.js","../src/core/views/navigation/Sidebar.js","../src/core/views/navigation/PageHeader.js","../src/core/pages/DeniedPage.js","../src/core/pages/NotFoundPage.js","../src/core/PortalApp.js","../src/core/forms/FormPage.js","../src/core/utils/MustacheFormatter.js","../src/index.js"],"sourcesContent":["/**\n * ConsoleSilencer\n * \n * Purpose:\n * - Reduce console noise by filtering out non-critical logs in production.\n * - Keep critical logs (warn, error) by default in production.\n * - Allow runtime override via API, URL params, or localStorage without rebuilding.\n *\n * Usage:\n * import ConsoleSilencer from '@core/utils/ConsoleSilencer.js';\n * ConsoleSilencer.install(); // defaults to 'warn' in production, 'debug' in dev\n *\n * // Optional: set level at install-time\n * ConsoleSilencer.install({ level: 'error' });\n *\n * // Change level at runtime (optionally persist to localStorage)\n * ConsoleSilencer.setLevel('debug', { persist: true });\n *\n * // Temporarily change level around a block\n * ConsoleSilencer.withTemporaryLevel('debug', () => {\n * // noisy diagnostics here\n * });\n *\n * Runtime overrides (no code changes needed):\n * - URL: ?logLevel=debug|info|warn|error|silent\n * also accepts mojoLog and loglevel as param names\n * - localStorage: localStorage.setItem('MOJO_LOG_LEVEL', 'debug')\n *\n * Notes:\n * - This module is side-effect free until install() is called.\n * - Idempotent: calling install() multiple times won't double-wrap console.\n */\n\nconst LEVELS = Object.freeze({\n silent: 0,\n error: 1,\n warn: 2,\n info: 3,\n log: 3, // alias to info\n debug: 4,\n trace: 5,\n all: 5, // alias\n});\n\n// Resolve environment in a bundler/browser-safe way\n// __DEV__ can be injected by bundlers (e.g., Vite define)\nconst isDev = (() => {\n // Vite/ESM: import.meta.env.DEV if available\n try {\n /* eslint-disable-next-line no-undef */\n if (typeof import.meta !== 'undefined' && import.meta && import.meta.env && typeof import.meta.env.DEV !== 'undefined') {\n return !!import.meta.env.DEV;\n }\n } catch {\n // ignore\n }\n\n // Prefer global __DEV__ if present (e.g., defined via bundler define)\n if (typeof globalThis !== 'undefined' && typeof globalThis.__DEV__ !== 'undefined') {\n try {\n return !!globalThis.__DEV__;\n } catch {\n // fall through if __DEV__ is not directly evaluable\n }\n }\n\n // Fallback detection (best effort)\n const hasProcess = typeof process !== 'undefined' && process && typeof process.env === 'object';\n if (hasProcess && typeof process.env.NODE_ENV === 'string') {\n return process.env.NODE_ENV !== 'production';\n }\n // Default conservative behavior: assume production if unknown\n return false;\n})();\n\nconst isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\nconst GLOBAL = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : global);\n\n// Capture the original console just once\nconst ORIGINAL_CONSOLE = GLOBAL.console || {};\nconst ORIGINALS = {};\nlet INSTALLED = false;\nlet CURRENT_LEVEL = null;\n\n// Default levels\nconst DEFAULT_DEV_LEVEL = 'debug';\nconst DEFAULT_PROD_LEVEL = 'warn'; // keep warn + error by default\n\n// Helper: normalize/parse level\nfunction parseLevel(level) {\n if (typeof level === 'number') {\n // Clamp to bounds\n const min = LEVELS.silent;\n const max = LEVELS.trace;\n return Math.min(Math.max(level, min), max);\n }\n if (typeof level === 'string') {\n const key = level.toLowerCase();\n if (Object.prototype.hasOwnProperty.call(LEVELS, key)) {\n return LEVELS[key];\n }\n }\n return null;\n}\n\n// Helper: detect override via URLSearchParams\nfunction getUrlLogLevel() {\n if (!isBrowser || typeof location === 'undefined' || !location.search) return null;\n try {\n const params = new URLSearchParams(location.search);\n const keys = ['logLevel', 'loglevel', 'mojoLog'];\n for (const k of keys) {\n const v = params.get(k);\n if (v != null) {\n const parsed = parseLevel(v);\n if (parsed !== null) return parsed;\n }\n }\n } catch {\n // ignore\n }\n return null;\n}\n\n// Helper: detect override via localStorage\nfunction getStoredLogLevel() {\n if (!isBrowser || !('localStorage' in GLOBAL)) return null;\n try {\n const v = GLOBAL.localStorage.getItem('MOJO_LOG_LEVEL');\n if (v != null) {\n const parsed = parseLevel(v);\n if (parsed !== null) return parsed;\n }\n } catch {\n // ignore storage errors\n }\n return null;\n}\n\nfunction storeLogLevel(levelNumberOrName) {\n if (!isBrowser || !('localStorage' in GLOBAL)) return;\n try {\n const key = typeof levelNumberOrName === 'string'\n ? levelNumberOrName\n : levelNumberOrName === null\n ? null\n : Object.entries(LEVELS).find(([, num]) => num === levelNumberOrName)?.[0] ?? null;\n if (key) {\n GLOBAL.localStorage.setItem('MOJO_LOG_LEVEL', key);\n } else {\n GLOBAL.localStorage.removeItem('MOJO_LOG_LEVEL');\n }\n } catch {\n // ignore storage errors\n }\n}\n\n// Build a wrapped method that checks the current level before calling through\nfunction makeWrapper(methodName, methodLevel) {\n const original = ORIGINALS[methodName] || ORIGINAL_CONSOLE[methodName] || (() => {});\n return function wrappedConsoleMethod(...args) {\n // If the method is allowed at the current level, call through\n if (CURRENT_LEVEL >= methodLevel) {\n return original.apply(ORIGINAL_CONSOLE, args);\n }\n // Otherwise, noop\n return undefined;\n };\n}\n\n// Special wrapper for assert: only logs when assertion fails\nfunction makeAssertWrapper() {\n const original = ORIGINALS.assert || ORIGINAL_CONSOLE.assert || (() => {});\n return function wrappedAssert(condition, ...args) {\n // Only logs when condition is falsy; treat failed assert like error-level\n if (!condition) {\n if (CURRENT_LEVEL >= LEVELS.error) {\n return original.apply(ORIGINAL_CONSOLE, [condition, ...args]);\n }\n return undefined;\n }\n // When assertion passes, no-op\n return undefined;\n };\n}\n\nfunction determineInitialLevel(explicitLevel) {\n // 1) Explicit install option\n const explicit = parseLevel(explicitLevel);\n if (explicit !== null) return explicit;\n\n // 2) URL override\n const urlLevel = getUrlLogLevel();\n if (urlLevel !== null) return urlLevel;\n\n // 3) localStorage override\n const storedLevel = getStoredLogLevel();\n if (storedLevel !== null) return storedLevel;\n\n // 4) Environment default\n return parseLevel(isDev ? DEFAULT_DEV_LEVEL : DEFAULT_PROD_LEVEL);\n}\n\n// Construct a patched console that respects CURRENT_LEVEL\nfunction buildPatchedConsole() {\n const patched = { ...ORIGINAL_CONSOLE };\n\n // Save originals once\n const methodLevels = {\n // Critical\n error: LEVELS.error,\n warn: LEVELS.warn,\n\n // Informational\n info: LEVELS.info,\n log: LEVELS.info,\n dir: LEVELS.info,\n table: LEVELS.info,\n\n // Verbose / Debug\n debug: LEVELS.debug,\n group: LEVELS.debug,\n groupCollapsed: LEVELS.debug,\n groupEnd: LEVELS.debug,\n time: LEVELS.debug,\n timeEnd: LEVELS.debug,\n timeLog: LEVELS.debug,\n trace: LEVELS.trace,\n };\n\n // Capture originals and build wrappers\n for (const name of Object.keys(methodLevels)) {\n ORIGINALS[name] = ORIGINAL_CONSOLE[name] || (() => {});\n patched[name] = makeWrapper(name, methodLevels[name]);\n }\n\n // Special-case assert\n ORIGINALS.assert = ORIGINAL_CONSOLE.assert || (() => {});\n patched.assert = makeAssertWrapper();\n\n // Preserve any other console methods as-is (clear, profile, count, etc.)\n return patched;\n}\n\nconst ConsoleSilencer = {\n // Install the silencer (idempotent)\n install(options = {}) {\n if (INSTALLED) {\n // Already installed; update level if provided\n if (options && typeof options.level !== 'undefined') {\n this.setLevel(options.level, { persist: !!options.persist });\n }\n return this;\n }\n\n if (!GLOBAL || !ORIGINAL_CONSOLE) {\n // No console available; nothing to do\n INSTALLED = true; // Prevent re-entry\n return this;\n }\n\n CURRENT_LEVEL = determineInitialLevel(options.level);\n\n const patched = buildPatchedConsole();\n // Replace the global console\n GLOBAL.console = patched;\n\n INSTALLED = true;\n\n // Expose for quick manual access if desired\n GLOBAL.MOJOConsoleSilencer = this;\n\n return this;\n },\n\n // Uninstall and restore the original console\n uninstall() {\n if (!INSTALLED) return this;\n\n try {\n GLOBAL.console = ORIGINAL_CONSOLE;\n } catch {\n // ignore\n }\n INSTALLED = false;\n return this;\n },\n\n // Set current level at runtime; accepts string or number\n // levels: 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace'\n setLevel(level, { persist = false } = {}) {\n const parsed = parseLevel(level);\n if (parsed === null) return this; // ignore invalid level\n CURRENT_LEVEL = parsed;\n if (persist) {\n storeLogLevel(level);\n }\n return this;\n },\n\n // Get the current numeric level\n getLevel() {\n return CURRENT_LEVEL;\n },\n\n // Get the current level name (best-effort)\n getLevelName() {\n const entry = Object.entries(LEVELS).find(([, num]) => num === CURRENT_LEVEL);\n return entry ? entry[0] : null;\n },\n\n // Convenience helpers\n criticalOnly({ persist = false } = {}) {\n return this.setLevel('warn', { persist });\n },\n\n errorsOnly({ persist = false } = {}) {\n return this.setLevel('error', { persist });\n },\n\n silent({ persist = false } = {}) {\n return this.setLevel('silent', { persist });\n },\n\n verbose({ persist = false } = {}) {\n return this.setLevel(isDev ? 'debug' : 'info', { persist });\n },\n\n allowAll({ persist = false } = {}) {\n return this.setLevel('trace', { persist });\n },\n\n // Run a block with a temporary level, then restore\n withTemporaryLevel(level, fn) {\n const prev = CURRENT_LEVEL;\n const parsed = parseLevel(level);\n if (parsed === null || typeof fn !== 'function') return fn?.();\n CURRENT_LEVEL = parsed;\n try {\n return fn();\n } finally {\n CURRENT_LEVEL = prev;\n }\n },\n\n // Expose levels map for consumers\n LEVELS,\n};\n\n// Optional: convenience named export alias\nexport default ConsoleSilencer;\nexport const installConsoleSilencer = (options) => ConsoleSilencer.install(options);","/**\n * GroupSearchView - Hierarchical tree-view search component\n * Extends SimpleSearchView to support parent/child relationships\n * Displays items in a tree structure based on parent property\n */\n\nimport SimpleSearchView from './SimpleSearchView.js';\n\nclass GroupSearchView extends SimpleSearchView {\n constructor(options = {}) {\n super({\n ...options,\n className: `group-search-view ${options.className || ''}`.trim()\n });\n\n // Tree-specific configuration\n this.showKind = options.showKind !== undefined ? options.showKind : true;\n this.parentField = options.parentField || 'parent';\n this.kindField = options.kindField || 'kind';\n this.treeData = []; // Processed hierarchical data\n this.flattenedItems = []; // Flattened tree for display\n \n // Visual customization\n this.showLines = options.showLines !== undefined ? options.showLines : true;\n }\n\n /**\n * Build tree hierarchy from flat list\n */\n buildTreeHierarchy(items) {\n if (!items || items.length === 0) {\n return [];\n }\n\n // Create a map of items by ID for quick lookup\n const itemsById = new Map();\n \n // First pass: add all items and extract parent objects\n items.forEach(item => {\n if (!itemsById.has(item.id)) {\n itemsById.set(item.id, {\n ...item,\n children: [],\n level: 0,\n hasChildren: false\n });\n }\n \n // If this item has a parent object, add the parent too\n const parentObj = item[this.parentField];\n if (parentObj && parentObj.id && !itemsById.has(parentObj.id)) {\n itemsById.set(parentObj.id, {\n ...parentObj,\n children: [],\n level: 0,\n hasChildren: false\n });\n }\n });\n\n const rootItems = [];\n\n // Build parent-child relationships\n itemsById.forEach((treeItem, itemId) => {\n const originalItem = items.find(i => i.id === itemId) || treeItem;\n const parentId = originalItem[this.parentField]?.id;\n\n if (parentId && itemsById.has(parentId)) {\n const parent = itemsById.get(parentId);\n parent.children.push(treeItem);\n parent.hasChildren = true;\n } else {\n rootItems.push(treeItem);\n }\n });\n\n // Calculate levels recursively\n const calculateLevels = (nodes, level = 0) => {\n nodes.forEach(node => {\n node.level = level;\n if (node.children.length > 0) {\n node.children.sort((a, b) => (a.name || '').localeCompare(b.name || ''));\n calculateLevels(node.children, level + 1);\n }\n });\n };\n\n rootItems.sort((a, b) => (a.name || '').localeCompare(b.name || ''));\n calculateLevels(rootItems);\n\n return rootItems;\n }\n\n /**\n * Flatten tree structure for rendering\n * Tracks ancestor \"last child\" flags for proper line rendering\n */\n flattenTree(nodes, result = [], ancestorLastFlags = []) {\n nodes.forEach((node, index) => {\n node._isLastChild = index === nodes.length - 1;\n node._ancestorLastFlags = [...ancestorLastFlags];\n \n // Check if this node is the last descendant in its branch\n // (all ancestors are last children AND this node is last child with no children)\n const allAncestorsLast = ancestorLastFlags.every(flag => flag);\n node._isLastDescendant = allAncestorsLast && node._isLastChild && \n (!node.children || node.children.length === 0);\n \n result.push(node);\n \n if (node.children && node.children.length > 0) {\n const newFlags = [...ancestorLastFlags, node._isLastChild];\n this.flattenTree(node.children, result, newFlags);\n }\n });\n \n return result;\n }\n\n /**\n * Second pass: compute which vertical lines should continue for each item\n * \n * Vertical line at segment s shows if there are more siblings coming at level s+1\n * We need to look ahead until we find an item at level s+1 or shallower\n */\n computeVerticalLines(flatList) {\n for (let i = 0; i < flatList.length; i++) {\n const item = flatList[i];\n item._continueVertical = [];\n \n for (let s = 0; s < item.level; s++) {\n // Look ahead to find next item at level s+1 or shallower\n let foundSibling = false;\n for (let j = i + 1; j < flatList.length; j++) {\n const futureItem = flatList[j];\n if (futureItem.level === s + 1) {\n // Found a sibling at this level - vertical should continue\n foundSibling = true;\n break;\n } else if (futureItem.level <= s) {\n // Went back to ancestor level or shallower - no more siblings\n break;\n }\n // Keep searching if futureItem.level > s+1 (still in deeper subtree)\n }\n item._continueVertical[s] = foundSibling;\n }\n }\n }\n\n /**\n * Update filtered items with tree structure\n */\n updateFilteredItems() {\n if (!this.collection) {\n this.filteredItems = [];\n this.treeData = [];\n this.flattenedItems = [];\n return;\n }\n\n // Server-side filtering is handled in performSearch() (from parent class)\n const items = this.collection.toJSON();\n this.treeData = this.buildTreeHierarchy(items);\n this.flattenedItems = this.flattenTree(this.treeData);\n \n // Second pass: compute vertical line continuation\n this.computeVerticalLines(this.flattenedItems);\n \n // Set filteredItems to flattened tree for parent class compatibility\n this.filteredItems = this.flattenedItems;\n\n this.updateResultsView();\n }\n\n /**\n * Get tree-specific item template\n */\n getDefaultItemTemplate() {\n return `\n <div class=\"tree-item-content\">\n <div class=\"tree-item-name\">{{name}}</div>\n {{#showKind}}\n <div class=\"tree-item-kind\">{{kind}}</div>\n {{/showKind}}\n </div>\n `;\n }\n\n /**\n * Process item template with tree structure\n */\n processItemTemplate(item) {\n // Process the base template\n let content = this.itemTemplate;\n content = content.replace(/\\{\\{(\\w+)\\}\\}/g, (match, prop) => {\n if (prop === 'showKind') {\n return this.showKind ? 'true' : '';\n }\n return this.getNestedValue(item, prop) || '';\n });\n\n // Handle conditional sections for kind\n if (this.showKind) {\n content = content.replace(/\\{\\{#showKind\\}\\}(.*?)\\{\\{\\/showKind\\}\\}/gs, '$1');\n } else {\n content = content.replace(/\\{\\{#showKind\\}\\}.*?\\{\\{\\/showKind\\}\\}/gs, '');\n }\n\n // Build tree line segments\n // For an item at level N, we need N segments (one for each ancestor level)\n let lineSegments = '';\n if (this.showLines && item.level > 0) {\n for (let i = 0; i < item.level; i++) {\n const isLastSegment = (i === item.level - 1);\n \n if (isLastSegment) {\n // This segment connects directly to this item\n // Use _isLastChild to determine if this is └ or ├\n const segClass = item._isLastChild ? 'tree-seg tree-seg-last' : 'tree-seg tree-seg-mid';\n lineSegments += `<span class=\"${segClass}\"></span>`;\n } else {\n // Ancestor segment - vertical line continues if next item is deeper than this level\n const continueVertical = item._continueVertical && item._continueVertical[i];\n if (continueVertical) {\n lineSegments += `<span class=\"tree-seg tree-seg-vert\"></span>`;\n } else {\n lineSegments += `<span class=\"tree-seg\"></span>`;\n }\n }\n }\n }\n\n // Classes for the wrapper\n const hasChildren = item.hasChildren ? ' has-children' : '';\n const isLastChild = item._isLastChild ? ' is-last-child' : '';\n\n // Wrap in tree structure\n return `\n <div class=\"tree-item-wrapper${hasChildren}${isLastChild}\" data-tree-level=\"${item.level}\">\n <div class=\"tree-lines\">\n ${lineSegments}\n </div>\n <div class=\"tree-item-body flex-grow-1\">\n ${content}\n </div>\n </div>\n `;\n }\n\n /**\n * Get all root items\n */\n getRootItems() {\n return this.treeData;\n }\n\n /**\n * Get children of a specific node\n */\n getNodeChildren(nodeId) {\n const findNode = (nodes, targetId) => {\n for (const node of nodes) {\n if (node.id === targetId) {\n return node.children;\n }\n if (node.children.length > 0) {\n const found = findNode(node.children, targetId);\n if (found) return found;\n }\n }\n return null;\n };\n\n return findNode(this.treeData, nodeId) || [];\n }\n}\n\nexport default GroupSearchView;\n","/**\n * Sidebar - Simple sidebar navigation component for MOJO framework\n * Provides easy menu switching and dynamic configuration\n */\n\nimport View from '@core/View.js';\nimport GroupSearchView from './GroupSearchView.js';\nimport {GroupList, Group} from '@core/models/Group.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\n\nclass Sidebar extends View {\n constructor(options = {}) {\n super({\n tagName: 'nav',\n className: 'sidebar',\n id: 'sidebar',\n ...options\n });\n\n this.menus = new Map();\n this.activeMenuName = null;\n this.currentRoute = null;\n this.showToggle = options.showToggle; // Default to true\n this.isCollapsed = false;\n this.sidebarTheme = options.theme || 'sidebar-light';\n this.customView = null;\n if (this.options.groupHeader) this.groupHeader = this.options.groupHeader;\n\n // Group selector configuration\n // 'inline' (default) - replaces sidebar view\n // 'dialog' - opens in a modal dialog like TopNav\n this.groupSelectorMode = options.groupSelectorMode || 'inline';\n this.groupSelectorDialog = null;\n // Apply sidebar theme\n if (this.sidebarTheme) {\n this.addClass(this.sidebarTheme);\n }\n\n // Initialize menus\n this.initializeMenus(options);\n\n // Setup route change listeners like TopNav\n this.setupRouteListeners();\n\n // Auto-collapse on mobile if specified\n if (options.autoCollapseMobile !== false) {\n this.setupResponsiveBehavior();\n }\n }\n\n groupHeader = `\n {{#group.parent}}\n <div class=\"sidebar-parent-bar\" data-action=\"select-group-parent\">\n <div class=\"parent-info\">\n <span class=\"parent-label\">{{group.parent.kind}}:</span>\n <span class=\"parent-name collapsed-hidden\">{{group.parent.name}}</span>\n </div>\n <i class=\"bi bi-chevron-down parent-expand collapsed-hidden\"></i>\n </div>\n {{/group.parent}}\n <div class=\"sidebar-selected-group-row\" data-action=\"show-group-search\">\n <div class=\"selected-group-info\">\n <div class='selected-group-name collapsed-hidden'>{{group.name}}</div>\n <div class='selected-group-meta collapsed-hidden'>\n <span class=\"selected-group-kind\">{{group.kind}}</span>\n </div>\n </div>\n <i class=\"bi bi-chevron-down selected-group-chevron collapsed-hidden\"></i>\n </div>\n `;\n\n /**\n * Initialize sidebar and auto-switch to correct menu based on current route\n */\n async onInit() {\n await super.onInit();\n\n // Get current route from router\n const app = this.getApp();\n const router = app?.router;\n\n if (router) {\n const currentPath = router.getCurrentPath();\n if (currentPath) {\n this.autoSwitchToMenuForRoute(currentPath);\n }\n }\n\n // Initialize tooltips for nav items\n this.initializeTooltips();\n\n this.searchView = new GroupSearchView({\n noAppend: true,\n showExitButton: true,\n headerText: \"Select Group\",\n containerId: \"sidebar-search-container\",\n Collection: GroupList,\n itemTemplate: `\n <div class=\"p-3 border-bottom\">\n <div class=\"fw-semibold text-dark\">{{name}}</div>\n <small class=\"text-muted\">#{{id}} {{kind}}</small>\n </div>\n `\n });\n this.addChild(this.searchView);\n this.searchView.on(\"item:selected\", (evt) => {\n console.log(evt);\n this.getApp().setActiveGroup(evt.model);\n });\n this.searchView.on(\"exit\", (item) => {\n console.log(item);\n this.hideGroupSearch();\n });\n }\n\n showGroupSearch() {\n if (this.groupSelectorMode === 'dialog') {\n this.showGroupSearchDialog();\n } else {\n // Inline mode (default)\n this.setClass('sidebar');\n this.showSearch = true;\n this.render();\n }\n }\n\n hideGroupSearch() {\n if (this.groupSelectorMode === 'dialog') {\n if (this.groupSelectorDialog) {\n this.groupSelectorDialog.hide();\n }\n } else {\n // Inline mode\n this.setClass('sidebar');\n this.showSearch = false;\n this.render();\n }\n }\n\n onActionShowGroupSearch() {\n this.showGroupSearch();\n }\n\n async onActionSelectGroupParent() {\n // select-group-parent\n const group = this.getApp().activeGroup;\n const result = await Dialog.confirm(`Are you sure you want to navigate to the '${group.get(\"parent.name\")}'?`);\n if (result) {\n this.getApp().showLoading();\n let parent = new Group({id: group.get(\"parent.id\")});\n await parent.fetch();\n this.getApp().setActiveGroup(parent);\n this.getApp().hideLoading();\n }\n }\n\n /**\n * Show group selector in a dialog (like TopNav)\n */\n async showGroupSearchDialog() {\n // Create or reuse collection instance (like GroupSelectorButton does)\n const collection = new GroupList();\n\n // Create GroupSearchView instance matching GroupSelectorButton pattern\n const searchView = new GroupSearchView({\n Collection: GroupList,\n collection: collection, // Pass the collection instance\n searchFields: ['name'],\n headerText: null,\n searchPlaceholder: \"Search groups...\",\n headerIcon: null,\n maxHeight: Math.min(600, window.innerHeight - 200),\n showExitButton: false,\n showKind: true, // Show kind badges (default: true)\n parentField: 'parent', // Field containing parent object\n kindField: 'kind', // Field containing kind/type\n autoExpandRoot: true, // Auto-expand root items (default: true)\n autoExpandAll: false, // Auto-expand all nodes (default: false)\n indentSize: 20, // Pixels per level (default: 20)\n showLines: true,\n });\n\n // Create dialog\n this.groupSelectorDialog = new Dialog({\n body: searchView,\n size: 'md',\n header: null,\n noBodyPadding: true,\n scrollable: false,\n buttons: [],\n closeButton: true\n });\n\n // Listen for item selection\n searchView.on('item:selected', (evt) => {\n console.log(evt);\n this.getApp().setActiveGroup(evt.model);\n if (this.groupSelectorDialog) {\n this.groupSelectorDialog.hide();\n }\n });\n\n // Clean up dialog reference when closed\n this.groupSelectorDialog.on('hidden', () => {\n this.groupSelectorDialog.destroy();\n this.groupSelectorDialog = null;\n });\n\n // Render and show the dialog\n await this.groupSelectorDialog.render(true, document.body);\n this.groupSelectorDialog.show();\n }\n\n /**\n * Find and switch to the menu that contains the given route\n */\n autoSwitchToMenuForRoute(route) {\n // Search through all menus to find one that contains this route\n for (const [menuName, menuConfig] of this.menus) {\n if (menuConfig.groupKind && !this.getApp().activeGroup)\n continue;\n if (this.menuContainsRoute(menuConfig, route)) {\n // Switch to this menu\n this._setActiveMenu(menuName);\n this.currentRoute = route;\n\n // Clear all active states and set new active item\n this.clearAllActiveStates();\n this.setActiveItemByRoute(route);\n\n // Re-render to show changes\n this.render();\n\n console.log(`Auto-switched to menu '${menuName}' for route '${route}'`);\n\n // Emit event for any listeners\n this.emit('menu-auto-switched', {\n menuName,\n route,\n config: menuConfig,\n sidebar: this\n });\n\n return true;\n }\n }\n\n return false; // No menu found for this route\n }\n\n /**\n * Clear active state from all menu items in all menus\n */\n clearAllActiveStates() {\n for (const [menuName, menuConfig] of this.menus) {\n for (const item of menuConfig.items || []) {\n item.active = false;\n if (item.children) {\n for (const child of item.children) {\n child.active = false;\n }\n }\n }\n }\n }\n\n /**\n * Set active state for item matching the given route\n */\n setActiveItemByRoute(route) {\n const normalizeRoute = (r) => {\n if (!r) return '/';\n // Decode URL-encoded characters (like %2F -> /)\n const decoded = decodeURIComponent(r);\n return decoded.startsWith('/') ? decoded : `/${decoded}`;\n };\n\n const targetRoute = normalizeRoute(route);\n\n // Search through all menus\n for (const [menuName, menuConfig] of this.menus) {\n if (menuConfig.groupKind && !this.getApp().activeGroup)\n continue;\n for (const item of menuConfig.items || []) {\n // Check main item\n if (item.route) {\n const itemRoute = normalizeRoute(item.route);\n if (this.routesMatch(targetRoute, itemRoute)) {\n item.active = true;\n this.activeMenuItem = item;\n return true;\n }\n }\n\n // Check children\n if (item.children) {\n for (const child of item.children) {\n if (child.route) {\n const childRoute = normalizeRoute(child.route);\n if (this.routesMatch(targetRoute, childRoute)) {\n child.active = true;\n item.active = true; // Parent also active\n return true;\n }\n }\n }\n }\n }\n }\n\n return false;\n }\n\n /**\n * Check if a menu contains a specific route in its items or children\n */\n menuContainsRoute(menuConfig, route) {\n const normalizeRoute = (r) => {\n if (!r) return '/';\n // Decode URL-encoded characters (like %2F -> /)\n const decoded = decodeURIComponent(r);\n return decoded.startsWith('/') ? decoded : `/${decoded}`;\n };\n\n const targetRoute = normalizeRoute(route);\n\n // Check each item in the menu\n for (const item of menuConfig.items || []) {\n // Check main item\n if (item.route) {\n const itemRoute = normalizeRoute(item.route);\n if (this.routesMatch(targetRoute, itemRoute)) {\n return true;\n }\n }\n\n // Check children\n if (item.children) {\n for (const child of item.children) {\n if (child.route) {\n const childRoute = normalizeRoute(child.route);\n if (this.routesMatch(targetRoute, childRoute)) {\n return true;\n }\n }\n }\n }\n }\n\n return false;\n }\n\n /**\n * Check if two routes match (using same logic as isItemActive)\n */\n routesMatch(currentRoute, itemRoute) {\n return this.getApp().router.doRoutesMatch(currentRoute, itemRoute);\n }\n\n getTemplate() {\n if (this.customView) {\n return '<div class=\"sidebar-container\" id=\"sidebar-custom-view-container\"></div>';\n }\n if (this.showSearch) return this.getSearchTemplate();\n return this.getMenuTemplate();\n }\n\n getSearchTemplate() {\n return `\n <div class=\"sidebar-container\" id=\"sidebar-search-container\">\n </div>\n `;\n }\n\n getMenuTemplate() {\n return `\n <div class=\"sidebar-container\">\n {{#data.currentMenu}}\n <!-- Header -->\n {{#header}}\n <div class=\"sidebar-header\">\n {{{header}}}\n {{#showToggle}}\n <button class=\"sidebar-toggle\" data-action=\"toggle-sidebar\"\n aria-label=\"Toggle Sidebar\">\n <i class=\"bi bi-chevron-left toggle-icon\"></i>\n <i class=\"bi bi-chevron-right toggle-icon\"></i>\n </button>\n {{/showToggle}}\n </div>\n {{/header}}\n\n <!-- Navigation Items -->\n <div class=\"sidebar-body\">\n <ul class=\"nav nav-pills flex-column sidebar-nav\" id=\"sidebar-nav-menu\">\n {{#items}}\n {{>nav-item}}\n {{/items}}\n </ul>\n </div>\n\n <!-- Footer -->\n {{#footer}}\n <div class=\"sidebar-footer\">\n {{{footer}}}\n </div>\n {{/footer}}\n {{/data.currentMenu}}\n\n {{^data.currentMenu}}\n <div class=\"sidebar-empty\">\n <p class=\"text-danger text-center\">No menu configured</p>\n </div>\n {{/data.currentMenu}}\n </div>\n `;\n }\n\n /**\n * Get template partials for rendering\n */\n getPartials() {\n return {\n 'nav-item': `\n {{#isDivider}}\n {{>nav-divider}}\n {{/isDivider}}\n {{#isSpacer}}\n {{>nav-spacer}}\n {{/isSpacer}}\n {{#isLabel}}\n {{>nav-label}}\n {{/isLabel}}\n\n {{^isDivider}}\n {{^isSpacer}}\n {{^isLabel}}\n <li class=\"nav-item\">\n {{#hasChildren}}\n <!-- Item with submenu -->\n <a class=\"nav-link {{#active}}active{{/active}} has-children collapsed\"\n data-bs-toggle=\"collapse\"\n href=\"#collapse-{{id}}\"\n role=\"button\"\n aria-expanded=\"{{#active}}true{{/active}}{{^active}}false{{/active}}\"\n data-action=\"toggle-submenu\">\n {{#icon}}<i class=\"{{icon}} me-2\"></i>{{/icon}}\n <span class=\"nav-text\">{{text}}</span>\n {{#badge}}\n <span class=\"{{badge.class}} ms-auto\">{{badge.text}}</span>\n {{/badge}}\n <i class=\"bi bi-chevron-down nav-arrow ms-auto\"></i>\n </a>\n <div class=\"collapse {{#active}}show{{/active}}\" id=\"collapse-{{id}}\" data-bs-parent=\"#sidebar-nav-menu\">\n <ul class=\"nav flex-column nav-submenu\">\n {{#children}}\n <li class=\"nav-item\">\n <a class=\"nav-link {{#active}}active{{/active}}\"\n {{#action}}data-action=\"{{action}}\"{{/action}}\n {{#href}}href=\"{{href}}\"{{/href}}>\n {{#icon}}<i class=\"{{icon}} me-2\"></i>{{/icon}}\n <span class=\"nav-text\">{{text}}</span>\n {{#badge}}\n <span class=\"{{badge.class}} ms-auto\">{{badge.text}}</span>\n {{/badge}}\n </a>\n </li>\n {{/children}}\n </ul>\n </div>\n {{/hasChildren}}\n {{^hasChildren}}\n <!-- Simple item -->\n <a class=\"nav-link {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}\"\n {{#action}}{{^disabled}}data-action=\"{{action}}\"{{/disabled}}{{/action}}\n {{#href}}{{^disabled}}href=\"{{href}}\"{{/disabled}}{{/href}}>\n {{#icon}}<i class=\"{{icon}} me-2\"></i>{{/icon}}\n <span class=\"nav-text\">{{text}}</span>\n {{#badge}}\n <span class=\"{{badge.class}} ms-auto\">{{badge.text}}</span>\n {{/badge}}\n </a>\n {{/hasChildren}}\n </li>\n {{/isLabel}}\n {{/isSpacer}}\n {{/isDivider}}\n `,\n 'nav-divider': `\n <li class=\"nav-divider-item\">\n <hr class=\"nav-divider-line\">\n </li>\n `,\n 'nav-spacer': `\n <li class=\"nav-spacer-item\"></li>\n `,\n 'nav-label': `\n <li class=\"nav-item {{className}}\">\n <div class=\"nav-text px-3\">{{text}}</div>\n </li>\n `\n };\n }\n\n getGroupHeader() {\n return this.groupHeader;\n }\n\n /**\n * Add a menu configuration\n */\n addMenu(name, config) {\n if (config.groupKind && !config.header) {\n config.header = this.getGroupHeader();\n }\n\n this.menus.set(name, {\n name,\n groupKind: config.groupKind || null,\n header: config.header || null,\n footer: config.footer || null,\n items: config.items || [],\n data: config.data || {},\n className: config.className || \"sidebar sidebar-dark\"\n });\n\n\n // Set as active if it's the first menu\n if (!this.activeMenuName) {\n this._setActiveMenu(name);\n }\n\n return this;\n }\n\n _setActiveMenu(name) {\n this.showSearch = false;\n this.activeMenuName = name;\n const config = this.getCurrentMenuConfig();\n if (config.className) {\n this.setClass(config.className);\n } else {\n this.setClass('sidebar');\n }\n }\n\n /**\n * Set the active menu\n */\n async setActiveMenu(name) {\n if (!this.menus.has(name)) {\n console.warn(`Menu '${name}' not found`);\n return this;\n }\n\n const menuConfig = this.menus.get(name);\n\n if (menuConfig.groupKind) {\n this.lastGroupMenu = menuConfig;\n // Handle group kind logic here\n if (!this.getApp().activeGroup) {\n this.showGroupSearch();\n return;\n }\n }\n\n this._setActiveMenu(name);\n await this.render();\n // Emit event\n this.emit('menu-changed', {\n menuName: name,\n config: menuConfig,\n sidebar: this\n });\n\n return this;\n }\n\n getGroupMenu(group) {\n if (!group) {\n console.warn('No group provided');\n return null;\n }\n // Find menu by group.kind\n let targetMenu = this.lastGroupMenu;\n let anyGroupMenu = null;\n if (group._.kind) {\n for (const [menuName, menuConfig] of this.menus) {\n // Check if groupKind matches\n const matches = this._groupKindMatches(menuConfig.groupKind, group._.kind);\n\n if (matches) {\n targetMenu = menuConfig;\n break;\n } else if (menuConfig.groupKind === 'any') {\n anyGroupMenu = menuConfig;\n }\n }\n }\n\n if (!targetMenu) {\n return anyGroupMenu;\n }\n return targetMenu;\n }\n\n /**\n * Check if a groupKind matches the group's kind\n * Supports both single string and array of strings\n * @param {string|string[]} groupKind - Single kind or array of kinds\n * @param {string} kind - The group's kind to match\n * @returns {boolean} True if matches\n */\n _groupKindMatches(groupKind, kind) {\n if (!groupKind || !kind) return false;\n\n // Handle array of kinds\n if (Array.isArray(groupKind)) {\n return groupKind.includes(kind);\n }\n\n // Handle single kind string\n return groupKind === kind;\n }\n\n showMenuForGroup(group) {\n if (!group) {\n console.warn('No group provided');\n return;\n }\n // Find menu by group.kind\n let targetMenu = this.getGroupMenu(group);\n\n if (!targetMenu) {\n console.warn(`No menu found for group kind: ${group.kind}`);\n return;\n }\n this._setActiveMenu(targetMenu.name);\n this.render();\n // Emit event\n this.emit('menu-changed', {\n menuName: targetMenu.name,\n config: targetMenu,\n sidebar: this\n });\n return this;\n }\n\n /**\n * Get menu configuration\n */\n getMenuConfig(name) {\n return this.menus.get(name) || null;\n }\n\n /**\n * Get current active menu configuration\n */\n getCurrentMenuConfig() {\n return this.activeMenuName ? this.menus.get(this.activeMenuName) : null;\n }\n\n /**\n * Update menu configuration\n */\n updateMenu(name, updates) {\n const menu = this.menus.get(name);\n if (!menu) {\n console.warn(`Menu '${name}' not found`);\n return this;\n }\n\n // Deep merge updates\n Object.assign(menu, updates);\n\n // Re-render if this is the active menu\n if (this.activeMenuName === name) {\n this.render();\n }\n\n return this;\n }\n\n /**\n * Remove a menu\n */\n removeMenu(name) {\n this.menus.delete(name);\n\n // If this was the active menu, switch to another or clear\n if (this.activeMenuName === name) {\n const remainingMenus = Array.from(this.menus.keys());\n this.activeMenuName = remainingMenus.length > 0 ? remainingMenus[0] : null;\n this.render();\n }\n\n return this;\n }\n\n /**\n * Get view data for template rendering\n */\n async onBeforeRender() {\n const currentMenu = this.getCurrentMenuConfig();\n\n if (!currentMenu) {\n return { currentMenu: null };\n }\n\n let subData = {\n version: this.getApp().version || null,\n group: this.getApp().activeGroup || null,\n user: this.getApp.activeUser || null\n };\n // Process menu data through template if it contains handlebars\n this.data = {\n currentMenu: {\n header: this.renderTemplateString(currentMenu.header || '', subData),\n footer: this.renderTemplateString(currentMenu.footer || '', subData),\n items: this.processNavItems(currentMenu.items, currentMenu.groupKind),\n data: currentMenu.data,\n showToggle: this.showToggle\n }\n\n };\n }\n\n async onAfterRender() {\n // Re-initialize tooltips after render, but only if collapsed\n if (this.isCollapsedState()) {\n // Small delay to ensure DOM is fully rendered\n setTimeout(() => this.initializeTooltips(), 50);\n } else {\n this.destroyTooltips();\n }\n }\n\n setCustomView(view) {\n if (this.customView) {\n this.removeChild(this.customView.id);\n }\n this.customView = view;\n if (view) {\n view.containerId = 'sidebar-custom-view-container';\n this.addChild(view);\n }\n this.render();\n return this;\n }\n\n clearCustomView() {\n if (this.customView) {\n this.removeChild(this.customView.id);\n this.customView = null;\n }\n this.render();\n return this;\n }\n\n /**\n * Process navigation items - add IDs, active states, and proper hrefs\n */\n processNavItems(items, groupKind) {\n const app = this.getApp();\n const activeUser = app?.activeUser;\n const activeGroup = app?.activeGroup;\n\n // Helper function to normalize and update route with group parameter\n const updateRouteWithGroup = (route) => {\n let normalizedRoute = route;\n \n // Convert path format (/forms) to query string format (?page=forms)\n if (route.startsWith('/') && !route.includes('?')) {\n const pageName = route.substring(1) || 'home';\n normalizedRoute = `?page=${pageName}`;\n }\n \n // Add group parameter if needed\n if (groupKind && activeGroup && activeGroup.id) {\n const separator = normalizedRoute.includes('?') ? '&' : '?';\n return `${normalizedRoute}${separator}group=${activeGroup.id}`;\n }\n return normalizedRoute;\n };\n\n return items.map((item, index) => {\n // Handle divider items\n if (item === \"\" || (typeof item === 'object' && item.divider)) {\n return {\n isDivider: true,\n id: `divider-${index}`\n };\n }\n\n // Handle spacer items\n if (typeof item === 'object' && item.spacer) {\n return {\n isSpacer: true,\n id: `spacer-${index}`\n };\n }\n\n const processedItem = { ...item };\n\n // Check permissions - skip item if user doesn't have required permissions\n if (processedItem.permissions) {\n if (!activeUser || !activeUser.hasPermission(processedItem.permissions)) {\n return null; // Will be filtered out\n }\n }\n\n // Check requiresGroupKind - skip item if group kind doesn't match\n if (processedItem.requiresGroupKind) {\n const groupKind = activeGroup?._.kind || activeGroup?.kind;\n if (!groupKind || !this._groupKindMatches(processedItem.requiresGroupKind, groupKind)) {\n return null; // Will be filtered out\n }\n }\n\n if (processedItem.kind === 'label') {\n processedItem.isLabel = true;\n if (!processedItem.id) {\n processedItem.id = `nav-label-${index}`;\n }\n return processedItem;\n }\n\n // Generate ID if not provided\n if (!processedItem.id) {\n processedItem.id = `nav-${index}`;\n }\n\n // Use route directly as href (like TopNav does)\n if (processedItem.route) {\n processedItem.href = updateRouteWithGroup(processedItem.route);\n } else if (processedItem.page) {\n // If only page is provided, convert to route format\n const baseRoute = processedItem.page.startsWith('/') ? processedItem.page : `/${processedItem.page}`;\n processedItem.href = updateRouteWithGroup(baseRoute);\n processedItem.route = processedItem.href; // Store for active matching\n }\n\n // Active state is already set on the item object, no need to calculate\n\n // Process children\n if (processedItem.children) {\n processedItem.children = processedItem.children.map(child => {\n const processedChild = { ...child };\n\n // Check permissions for child items\n if (processedChild.permissions && activeUser) {\n if (!activeUser.hasPermission(processedChild.permissions)) {\n return null; // Will be filtered out\n }\n }\n\n // Check requiresGroupKind for child items\n if (processedChild.requiresGroupKind) {\n const groupKind = activeGroup?._.kind || activeGroup?.kind;\n if (!groupKind || !this._groupKindMatches(processedChild.requiresGroupKind, groupKind)) {\n return null; // Will be filtered out\n }\n }\n\n // Use route directly as href\n if (processedChild.route) {\n processedChild.href = updateRouteWithGroup(processedChild.route);\n } else if (processedChild.page) {\n const baseRoute = processedChild.page.startsWith('/') ? processedChild.page : `/${processedChild.page}`;\n processedChild.href = updateRouteWithGroup(baseRoute);\n processedChild.route = processedChild.href;\n }\n\n // Active state is already set on the child object\n return processedChild;\n }).filter(child => child !== null); // Filter out permission-denied children\n\n // Update hasChildren flag after filtering children\n processedItem.hasChildren = !!(processedItem.children && processedItem.children.length > 0);\n } else {\n // Add hasChildren flag for template logic\n processedItem.hasChildren = false;\n }\n\n return processedItem;\n }).filter(item => item !== null); // Filter out permission-denied items\n }\n\n\n\n\n /**\n * Check if navigation item should be active (similar to TopNav)\n */\n isItemActive(item) {\n if (!item.route || !this.currentRoute) {\n return false;\n }\n\n const normalizeRoute = (route) => {\n if (!route) return '/';\n // Decode URL-encoded characters (like %2F -> /)\n const decoded = decodeURIComponent(route);\n return decoded.startsWith('/') ? decoded : `/${decoded}`;\n };\n\n const itemRoute = normalizeRoute(item.route);\n const currentRoute = normalizeRoute(this.currentRoute);\n\n if (itemRoute === '/' && currentRoute === '/') {\n return true;\n }\n\n if (itemRoute !== '/' && currentRoute !== '/') {\n return currentRoute.startsWith(itemRoute) || currentRoute === itemRoute;\n }\n\n return false;\n }\n\n /**\n * Update active item based on current route (like TopNav)\n */\n async updateActiveItem(route) {\n this.currentRoute = route;\n\n // Clear all active states and set new active item\n this.clearAllActiveStates();\n this.setActiveItemByRoute(route);\n\n await this.render();\n return this;\n }\n\n /**\n * Action handler: Toggle submenu\n */\n async handleActionToggleSubmenu(event, element) {\n const arrow = element.querySelector('.nav-arrow');\n if (arrow) {\n arrow.classList.toggle('rotated');\n }\n }\n\n /**\n * Action handler: Toggle sidebar collapsed/expanded state\n */\n async handleActionToggleSidebar(event, element) {\n this.toggleSidebar();\n }\n\n onActionShowGroupMenu(action, event, el) {\n this.setActiveMenu(\"group_default\");\n\n return false;\n }\n\n async onActionDefault(action, event, el) {\n const config = this.getCurrentMenuConfig();\n if (!config) return;\n\n // Helper to recursively search for action in items and children\n const findAndExecuteHandler = (items) => {\n for (const item of items) {\n if ((item.action == action) && item.handler) {\n item.handler(action, event, el, this.getApp());\n return true;\n }\n // Check children recursively\n if (item.children && item.children.length > 0) {\n if (findAndExecuteHandler(item.children)) {\n return true;\n }\n }\n }\n return false;\n };\n\n return findAndExecuteHandler(config.items);\n }\n\n /**\n * Get all menu names\n */\n getMenuNames() {\n return Array.from(this.menus.keys());\n }\n\n /**\n * Check if menu exists\n */\n hasMenu(name) {\n return this.menus.has(name);\n }\n\n /**\n * Clear all menus\n */\n clearMenus() {\n this.menus.clear();\n this.activeMenuName = null;\n this.render();\n return this;\n }\n\n /**\n * Set data for current menu\n */\n setMenuData(data) {\n const currentMenu = this.getCurrentMenuConfig();\n if (currentMenu) {\n currentMenu.data = { ...currentMenu.data, ...data };\n this.render();\n }\n return this;\n }\n\n /**\n * Get data for current menu\n */\n getMenuData() {\n const currentMenu = this.getCurrentMenuConfig();\n return currentMenu ? currentMenu.data : {};\n }\n\n /**\n * Setup listeners for route change events (like TopNav)\n */\n setupRouteListeners() {\n const app = this.getApp();\n if (app && app.events) {\n app.events.on([\"page:showing\"], (data) => {\n this.onRouteChanged(data);\n });\n app.events.on(\"group:changed\", (data) => {\n this.showMenuForGroup(data.group);\n });\n app.events.on(\"portal:user-changed\", (data) => {\n this.render();\n });\n }\n }\n\n /**\n * Handle route changed event - auto-switch menu and update active item\n */\n /**\n * Handle route changed event.\n *\n * Resolution order for \"which menu to show\":\n * 1. The menu that contains the new route (autoSwitchToMenuForRoute).\n * 2. The menu named by `page.sidebarMenu` (declared on the Page class).\n * 3. The first non-group menu registered — prevents \"last-menu-wins\" default.\n *\n * Pages that don't belong to any menu are called \"homeless\" pages.\n * Declare `sidebarMenu = 'menuName'` on the Page class to pin them to a menu:\n *\n * class SettingsPage extends Page {\n * sidebarMenu = 'default'; // show 'default' sidebar on this page\n * }\n */\n onRouteChanged(data) {\n if (data.page && data.page.route) {\n const route = data.page.route;\n if (this.activeMenuItem && this.routesMatch(route, this.activeMenuItem.route)) {\n return;\n }\n\n // 1. Try to auto-switch to the menu that contains this route\n const switchedMenu = this.autoSwitchToMenuForRoute(route);\n\n if (switchedMenu) {\n console.log(`Route changed to '${route}', auto-switched menu`);\n return;\n }\n\n // 2. \"Homeless\" page — route is not in any menu.\n // Check if the page class declares a preferred sidebarMenu name.\n const preferredMenu = data.page.sidebarMenu || data.page.options?.sidebarMenu || null;\n if (preferredMenu && this.menus.has(preferredMenu)) {\n this._setActiveMenu(preferredMenu);\n this.clearAllActiveStates();\n this.render();\n console.log(`Homeless route '${route}' — switched to page-declared sidebarMenu '${preferredMenu}'`);\n return;\n }\n\n // 3. Fall back to the first non-group menu so we never\n // accidentally land on the last-registered menu.\n let fallbackMenu = null;\n for (const [menuName, menuConfig] of this.menus) {\n if (!menuConfig.groupKind) {\n fallbackMenu = menuName;\n break;\n }\n }\n\n if (fallbackMenu && this.activeMenuName !== fallbackMenu) {\n this._setActiveMenu(fallbackMenu);\n console.log(`Homeless route '${route}' — fell back to first non-group menu '${fallbackMenu}'`);\n }\n\n // Always clear active states and re-render for homeless pages\n this.clearAllActiveStates();\n this.updateActiveItem(route);\n this.render();\n }\n }\n\n /**\n * Toggle sidebar between collapsed and expanded states\n */\n toggleSidebar() {\n const portalContainer = document.querySelector('.portal-container');\n if (!portalContainer) return;\n\n // Hide any visible tooltips before state change\n this.hideAllTooltips();\n\n const isCurrentlyCollapsed = portalContainer.classList.contains('collapse-sidebar');\n const isCurrentlyHidden = portalContainer.classList.contains('hide-sidebar');\n\n if (isCurrentlyHidden) {\n // Hidden -> Normal\n portalContainer.classList.remove('hide-sidebar');\n this.isCollapsed = false;\n this.destroyTooltips();\n } else if (isCurrentlyCollapsed) {\n // Collapsed -> Normal\n portalContainer.classList.remove('collapse-sidebar');\n this.isCollapsed = false;\n this.destroyTooltips();\n } else {\n // Normal -> Collapsed\n portalContainer.classList.add('collapse-sidebar');\n this.isCollapsed = true;\n // Initialize tooltips with delay to ensure DOM is ready\n setTimeout(() => this.initializeTooltips(), 150);\n }\n\n return this;\n }\n\n /**\n * Set sidebar state programmatically\n */\n setSidebarState(state) {\n const portalContainer = document.querySelector('.portal-container');\n if (!portalContainer) return this;\n\n // Remove all state classes first\n portalContainer.classList.remove('collapse-sidebar', 'hide-sidebar');\n\n switch (state) {\n case 'collapsed':\n portalContainer.classList.add('collapse-sidebar');\n this.isCollapsed = true;\n break;\n case 'hidden':\n portalContainer.classList.add('hide-sidebar');\n this.isCollapsed = false;\n break;\n case 'normal':\n default:\n this.isCollapsed = false;\n break;\n }\n\n // Handle tooltips based on state\n if (this.isCollapsed) {\n // Hide any visible tooltips first\n this.hideAllTooltips();\n // Initialize tooltips when collapsed\n setTimeout(() => this.initializeTooltips(), 100);\n } else {\n // Destroy tooltips when not collapsed\n this.destroyTooltips();\n }\n\n return this;\n }\n\n /**\n * Initialize tooltips for nav items when sidebar is collapsed\n */\n initializeTooltips() {\n // Clean up existing tooltips first\n this.destroyTooltips();\n\n // Only initialize tooltips in collapsed state\n if (!this.isCollapsedState()) {\n return this;\n }\n\n // Auto-generate tooltips from nav-text content\n const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link');\n\n navLinks.forEach((link) => {\n const navText = link.querySelector('.nav-text');\n\n if (navText && navText.textContent.trim()) {\n const tooltipText = navText.textContent.trim();\n\n // Set Bootstrap tooltip attributes\n link.setAttribute('data-bs-toggle', 'tooltip');\n link.setAttribute('data-bs-placement', 'right');\n link.setAttribute('data-bs-title', tooltipText);\n link.setAttribute('data-bs-container', 'body');\n\n // Initialize Bootstrap tooltip with better config\n if (window.bootstrap && window.bootstrap.Tooltip) {\n // Extract custom theme/size if specified\n const theme = link.getAttribute('data-tooltip-theme');\n const size = link.getAttribute('data-tooltip-size');\n\n // Build custom class list\n let customClass = '';\n if (theme) customClass += `tooltip-${theme} `;\n if (size) customClass += `tooltip-${size}`;\n\n // Build options object\n const tooltipOptions = {\n placement: 'right',\n container: 'body',\n trigger: 'hover',\n delay: { show: 500, hide: 100 },\n fallbackPlacements: ['top', 'bottom', 'left']\n };\n\n // Only add customClass if it has a value\n const trimmedClass = customClass.trim();\n if (trimmedClass) {\n tooltipOptions.customClass = trimmedClass;\n }\n\n const tooltip = new window.bootstrap.Tooltip(link, tooltipOptions);\n\n // Store tooltip instance for better management\n link._tooltipInstance = tooltip;\n\n // Add event listeners to prevent stuck tooltips\n link.addEventListener('click', () => {\n tooltip.hide();\n });\n\n link.addEventListener('blur', () => {\n tooltip.hide();\n });\n }\n }\n });\n\n // Add global event listeners to hide tooltips\n this.addTooltipHideListeners();\n\n return this;\n }\n\n destroyTooltips() {\n // Remove global tooltip hide listeners\n this.removeTooltipHideListeners();\n\n const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle=\"tooltip\"]');\n\n navLinks.forEach((link) => {\n // Use stored instance first, then try to get it\n const tooltipInstance = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);\n if (tooltipInstance) {\n // Force hide before dispose\n tooltipInstance.hide();\n tooltipInstance.dispose();\n }\n\n // Clean up stored reference\n delete link._tooltipInstance;\n\n // Remove tooltip attributes\n link.removeAttribute('data-bs-toggle');\n link.removeAttribute('data-bs-placement');\n link.removeAttribute('data-bs-title');\n link.removeAttribute('data-bs-container');\n });\n\n return this;\n }\n\n /**\n * Get current sidebar state\n */\n getSidebarState() {\n const portalContainer = document.querySelector('.portal-container');\n if (!portalContainer) return 'normal';\n\n if (portalContainer.classList.contains('hide-sidebar')) {\n return 'hidden';\n } else if (portalContainer.classList.contains('collapse-sidebar')) {\n return 'collapsed';\n } else {\n return 'normal';\n }\n }\n\n /**\n * Check if sidebar is collapsed\n */\n isCollapsedState() {\n return this.getSidebarState() === 'collapsed';\n }\n\n /**\n * Enable/disable toggle button\n */\n setToggleEnabled(enabled) {\n this.showToggle = enabled;\n this.render();\n return this;\n }\n\n /**\n * Initialize menus from options\n */\n initializeMenus(options) {\n if (options.menus) {\n for (const menu of options.menus) {\n this.addMenu(menu.name, menu);\n }\n } else if (options.menu) {\n options.menu.name = options.menu.name || \"default\";\n this.addMenu(options.menu.name, options.menu);\n }\n }\n\n /**\n * Add global listeners to hide tooltips when needed\n */\n addTooltipHideListeners() {\n // Hide tooltips on scroll\n this._tooltipScrollHandler = () => this.hideAllTooltips();\n this.element.addEventListener('scroll', this._tooltipScrollHandler, { passive: true });\n\n // Hide tooltips on route change\n this._tooltipRouteHandler = () => this.hideAllTooltips();\n const app = this.getApp();\n\n\n // Hide tooltips on window blur\n this._tooltipBlurHandler = () => this.hideAllTooltips();\n window.addEventListener('blur', this._tooltipBlurHandler);\n\n // Hide tooltips on escape key\n this._tooltipEscapeHandler = (e) => {\n if (e.key === 'Escape') {\n this.hideAllTooltips();\n }\n };\n document.addEventListener('keydown', this._tooltipEscapeHandler);\n }\n\n /**\n * Remove global tooltip hide listeners\n */\n removeTooltipHideListeners() {\n if (this._tooltipScrollHandler) {\n this.element.removeEventListener('scroll', this._tooltipScrollHandler);\n delete this._tooltipScrollHandler;\n }\n\n if (this._tooltipBlurHandler) {\n window.removeEventListener('blur', this._tooltipBlurHandler);\n delete this._tooltipBlurHandler;\n }\n\n if (this._tooltipEscapeHandler) {\n document.removeEventListener('keydown', this._tooltipEscapeHandler);\n delete this._tooltipEscapeHandler;\n }\n }\n\n /**\n * Force hide all visible tooltips\n */\n hideAllTooltips() {\n const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle=\"tooltip\"]');\n navLinks.forEach((link) => {\n const tooltip = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);\n if (tooltip) {\n tooltip.hide();\n }\n });\n\n // Also hide any orphaned tooltips\n const visibleTooltips = document.querySelectorAll('.tooltip.show');\n visibleTooltips.forEach(tooltip => {\n tooltip.remove();\n });\n }\n\n /**\n * Cleanup on destroy\n */\n async onBeforeDestroy() {\n // Clean up tooltips\n this.destroyTooltips();\n\n // Call parent cleanup\n await super.onBeforeDestroy();\n }\n\n /**\n * Setup responsive behavior for mobile\n */\n setupResponsiveBehavior() {\n const checkMobile = () => {\n const isMobile = window.innerWidth <= 768;\n const portalContainer = document.querySelector('.portal-container');\n\n if (portalContainer) {\n if (isMobile) {\n portalContainer.classList.add('sidebar-mobile');\n } else {\n portalContainer.classList.remove('sidebar-mobile', 'sidebar-open');\n }\n }\n };\n\n // Check on load and resize\n checkMobile();\n window.addEventListener('resize', checkMobile);\n }\n\n /**\n * Static method to create a sidebar with common configuration\n */\n static createDefault(options = {}) {\n return new Sidebar({\n theme: 'sidebar-clean',\n showToggle: true,\n autoCollapseMobile: true,\n ...options\n });\n }\n\n /**\n * Static method to create a minimal sidebar\n */\n static createMinimal(options = {}) {\n return new Sidebar({\n theme: 'sidebar-clean',\n showToggle: false,\n autoCollapseMobile: false,\n ...options\n });\n }\n\n /**\n * Set sidebar theme\n */\n setSidebarTheme(theme) {\n // Remove existing theme classes\n this.removeClass('sidebar-light sidebar-dark sidebar-clean');\n\n // Add new theme\n this.sidebarTheme = theme;\n this.addClass(theme);\n\n return this;\n }\n\n /**\n * Quick method to show/hide the sidebar\n */\n show() {\n return this.setSidebarState('normal');\n }\n\n hide() {\n return this.setSidebarState('hidden');\n }\n\n collapse() {\n return this.setSidebarState('collapsed');\n }\n\n expand() {\n return this.setSidebarState('normal');\n }\n\n /**\n * Add pulse effect to toggle button\n */\n pulseToggle() {\n const toggleButton = this.element.querySelector('.sidebar-toggle');\n if (toggleButton) {\n toggleButton.classList.add('pulse');\n\n // Remove pulse after 3 seconds or first click\n const removePulse = () => {\n toggleButton.classList.remove('pulse');\n toggleButton.removeEventListener('click', removePulse);\n };\n\n toggleButton.addEventListener('click', removePulse, { once: true });\n setTimeout(removePulse, 3000);\n }\n return this;\n }\n\n /**\n * Utility method to quickly add a simple menu item\n */\n addSimpleMenuItem(menuName, text, route, icon = 'bi-circle') {\n const menu = this.menus.get(menuName);\n if (menu) {\n menu.items = menu.items || [];\n menu.items.push({\n text: text,\n route: route,\n icon: icon\n });\n\n if (this.activeMenuName === menuName) {\n this.render();\n }\n }\n return this;\n }\n\n /**\n * Utility method to quickly create and set a simple menu\n */\n setSimpleMenu(name, header, items) {\n const menu = {\n name: name,\n header: header,\n items: items\n };\n\n this.addMenu(name, menu);\n this.setActiveMenu(name);\n\n return this;\n }\n\n}\n\nexport default Sidebar;\n","/**\n * PageHeader - Displays page title, description, and actions above page content\n * Used by PortalApp to show consistent page headers\n */\n\nimport View from '@core/View.js';\n\nclass PageHeader extends View {\n constructor(options = {}) {\n super({\n tagName: 'div',\n className: 'page-header',\n ...options\n });\n\n // Configuration\n this.style = options.style || 'default'; // 'default' | 'minimal' | 'breadcrumb'\n this.size = options.size || 'md'; // 'sm' | 'md' | 'lg' | 'xl'\n this.showIcon = options.showIcon !== false;\n this.showDescription = options.showDescription !== false;\n this.showBreadcrumbs = options.showBreadcrumbs || false;\n \n // Current page reference\n this.currentPage = null;\n }\n\n async getTemplate() {\n if (this.style === 'minimal') {\n return this.getMinimalTemplate();\n } else if (this.style === 'breadcrumb') {\n return this.getBreadcrumbTemplate();\n }\n return this.getDefaultTemplate();\n }\n\n getDefaultTemplate() {\n return `\n {{#data.hasPage}}\n <div class=\"page-header-content page-header-{{data.size}}\">\n <div class=\"page-header-main\">\n <div class=\"page-header-info\">\n {{#data.showIcon}}\n {{#data.pageIcon}}\n <div class=\"page-icon\">\n <i class=\"{{data.pageIcon}}\"></i>\n </div>\n {{/data.pageIcon}}\n {{/data.showIcon}}\n \n <div class=\"page-title-group\">\n <h1 class=\"page-title\">{{data.pageTitle}}</h1>\n {{#data.showDescription}}\n {{#data.pageDescription}}\n <p class=\"page-description text-muted\">{{data.pageDescription}}</p>\n {{/data.pageDescription}}\n {{/data.showDescription}}\n </div>\n </div>\n\n {{#data.hasActions}}\n <div class=\"page-actions\">\n {{#data.actions}}\n <button class=\"btn {{buttonClass}}\" \n data-action=\"{{action}}\"\n type=\"button\">\n {{#icon}}<i class=\"{{icon}} me-1\"></i>{{/icon}}\n {{label}}\n </button>\n {{/data.actions}}\n </div>\n {{/data.hasActions}}\n </div>\n </div>\n {{/data.hasPage}}\n `;\n }\n\n getMinimalTemplate() {\n return `\n {{#data.hasPage}}\n <div class=\"page-header-content page-header-minimal\">\n <h1 class=\"page-title\">\n {{#data.showIcon}}\n {{#data.pageIcon}}<i class=\"{{data.pageIcon}} me-2\"></i>{{/data.pageIcon}}\n {{/data.showIcon}}\n {{data.pageTitle}}\n </h1>\n </div>\n {{/data.hasPage}}\n `;\n }\n\n getBreadcrumbTemplate() {\n return `\n {{#data.hasPage}}\n <div class=\"page-header-content page-header-breadcrumb\">\n {{#data.showBreadcrumbs}}\n <nav aria-label=\"breadcrumb\">\n <ol class=\"breadcrumb mb-2\">\n {{#data.breadcrumbs}}\n <li class=\"breadcrumb-item {{#active}}active{{/active}}\">\n {{#href}}<a href=\"{{href}}\">{{label}}</a>{{/href}}\n {{^href}}{{label}}{{/href}}\n </li>\n {{/data.breadcrumbs}}\n </ol>\n </nav>\n {{/data.showBreadcrumbs}}\n \n <div class=\"d-flex justify-content-between align-items-start\">\n <h1 class=\"page-title\">\n {{#data.showIcon}}\n {{#data.pageIcon}}<i class=\"{{data.pageIcon}} me-2\"></i>{{/data.pageIcon}}\n {{/data.showIcon}}\n {{data.pageTitle}}\n </h1>\n \n {{#data.hasActions}}\n <div class=\"page-actions\">\n {{#data.actions}}\n <button class=\"btn {{buttonClass}}\" \n data-action=\"{{action}}\"\n type=\"button\">\n {{#icon}}<i class=\"{{icon}} me-1\"></i>{{/icon}}\n {{label}}\n </button>\n {{/data.actions}}\n </div>\n {{/data.hasActions}}\n </div>\n \n {{#data.showDescription}}\n {{#data.pageDescription}}\n <p class=\"page-description text-muted mt-2\">{{data.pageDescription}}</p>\n {{/data.pageDescription}}\n {{/data.showDescription}}\n </div>\n {{/data.hasPage}}\n `;\n }\n\n async onBeforeRender() {\n await super.onBeforeRender();\n\n const page = this.currentPage;\n const hasPage = !!page;\n\n // Debug logging\n if (page) {\n console.log('PageHeader page:', {\n title: page.title,\n displayName: page.displayName,\n name: page.name,\n pageName: page.pageName,\n icon: page.icon,\n pageIcon: page.pageIcon,\n pageDescription: page.pageDescription,\n description: page.description\n });\n }\n\n // Get headerActions from page options, instance, or constructor.prototype\n const headerActions = page?.options?.headerActions ||\n page?.headerActions || \n page?.constructor?.prototype?.headerActions || \n [];\n\n this.data = {\n hasPage,\n pageTitle: page?.title || page?.displayName || page?.name || page?.pageName || '',\n pageIcon: page?.icon || page?.pageIcon || '',\n pageDescription: page?.pageDescription || page?.description || '',\n showIcon: this.showIcon,\n showDescription: this.showDescription,\n showBreadcrumbs: this.showBreadcrumbs,\n breadcrumbs: page?.options?.breadcrumbs || page?.breadcrumbs || [],\n actions: headerActions,\n hasActions: headerActions.length > 0,\n size: this.size\n };\n \n console.log('PageHeader data:', this.data);\n }\n\n /**\n * Set the current page to display\n */\n async setPage(page) {\n console.log('PageHeader.setPage called with:', page?.pageName || page?.name || 'no page');\n this.currentPage = page;\n // Always render if we have a page, even if not yet mounted\n // This handles the case where setPage is called during initial app setup\n if (page) {\n console.log('PageHeader.setPage calling render()');\n await this.render();\n console.log('PageHeader.setPage render() complete');\n }\n }\n\n /**\n * Get the current page\n */\n getPage() {\n return this.currentPage;\n }\n\n /**\n * Handle action clicks from page header buttons\n */\n async onActionDefault(action, event, element) {\n // Emit to page if it has a handler\n if (this.currentPage && typeof this.currentPage.onHeaderAction === 'function') {\n await this.currentPage.onHeaderAction(action, event, element);\n return true;\n }\n\n // Emit event for parent to handle\n this.emit('action', {\n action,\n event,\n element,\n page: this.currentPage\n });\n\n return false;\n }\n}\n\nexport default PageHeader;\n","/**\n * DeniedPage - Access Denied page for MOJO Framework\n * Displays when a user attempts to access a page without proper permissions\n */\n\nimport Page from '@core/Page.js';\n\nclass DeniedPage extends Page {\n constructor(options = {}) {\n super({\n pageName: 'Access Denied',\n route: '/denied',\n title: 'Access Denied',\n pageIcon: 'bi bi-shield-x',\n template: `\n <div class=\"container mt-5\">\n <div class=\"row justify-content-center\">\n <div class=\"col-md-8 col-lg-6\">\n <div class=\"text-center mb-4\">\n <i class=\"bi bi-shield-x text-muted\" style=\"font-size: 3rem;\"></i>\n <h2 class=\"mt-3 mb-2\">Access Denied</h2>\n <p class=\"text-muted\">You don't have permission to access this page.</p>\n </div>\n\n {{#deniedPage}}\n <div class=\"card border-0 shadow-sm mb-4\">\n <div class=\"card-body\">\n <h6 class=\"card-subtitle mb-2 text-muted\">Requested Page</h6>\n <h5 class=\"card-title\">\n <i class=\"{{pageIcon}} me-2\"></i>\n {{displayName}}\n </h5>\n {{#route}}\n <p class=\"card-text text-muted small\">{{route}}</p>\n {{/route}}\n {{#description}}\n <p class=\"card-text\">{{description}}</p>\n {{/description}}\n\n {{#requiredPermissions}}\n <div class=\"mt-3\">\n <h6 class=\"mb-2\">Required Permissions:</h6>\n {{#permissions}}\n <span class=\"badge bg-light text-dark me-1 mb-1\">{{.}}</span>\n {{/permissions}}\n {{^permissions}}\n <span class=\"text-muted small\">Authentication required</span>\n {{/permissions}}\n </div>\n {{/requiredPermissions}}\n </div>\n </div>\n {{/deniedPage}}\n\n <div class=\"d-grid gap-2 d-md-flex justify-content-md-center\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"go-back\">\n <i class=\"bi bi-arrow-left me-1\"></i>\n Go Back\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"go-home\">\n <i class=\"bi bi-house me-1\"></i>\n Home\n </button>\n {{#showLogin}}\n <button type=\"button\" class=\"btn btn-outline-primary\" data-action=\"login\">\n <i class=\"bi bi-box-arrow-in-right me-1\"></i>\n Login\n </button>\n {{/showLogin}}\n </div>\n\n {{#currentUser}}\n <div class=\"text-center mt-4\">\n <small class=\"text-muted\">\n Logged in as <strong>{{username}}</strong>\n </small>\n </div>\n {{/currentUser}}\n </div>\n </div>\n </div>\n `,\n ...options\n });\n\n // Store the denied page instance\n this.deniedPage = null;\n this.deniedPageOptions = null;\n }\n\n /**\n * Handle route parameters - expect denied page info\n */\n async onParams(params = {}, query = {}) {\n await super.onParams(params, query);\n\n // If page info is passed in params\n if (params.page) {\n this.deniedPage = params.page;\n this.deniedPageOptions = params.page.options || params.page.pageOptions || {};\n } else if (query.page) {\n // Handle page name from query string\n this.deniedPageName = query.page;\n }\n }\n\n /**\n * Set the denied page instance\n */\n setDeniedPage(pageInstance) {\n this.deniedPage = pageInstance;\n this.deniedPageOptions = pageInstance?.options || pageInstance?.pageOptions || {};\n return this;\n }\n\n /**\n * Get view data for template rendering\n */\n async getViewData() {\n const app = this.getApp();\n\n // Get current user info\n const currentUser = app?.activeUser || app?.getCurrentUser?.() || null;\n\n // Process denied page info\n let deniedPageInfo = null;\n if (this.deniedPage) {\n const permissions = this.deniedPageOptions?.permissions ||\n this.deniedPage.options?.permissions ||\n this.deniedPage.pageOptions?.permissions;\n\n deniedPageInfo = {\n displayName: this.deniedPage.displayName || this.deniedPage.pageName || this.deniedPage.title || 'Unknown Page',\n pageName: this.deniedPage.pageName,\n route: this.deniedPage.route,\n description: this.deniedPage.pageDescription || this.deniedPage.description,\n pageIcon: this.deniedPage.pageIcon || 'bi bi-file-text',\n requiredPermissions: permissions ? {\n permissions: Array.isArray(permissions) ? permissions : [permissions]\n } : null\n };\n } else if (this.deniedPageName) {\n deniedPageInfo = {\n displayName: this.deniedPageName,\n pageName: this.deniedPageName,\n pageIcon: 'bi bi-file-text'\n };\n }\n\n return {\n deniedPage: deniedPageInfo,\n currentUser: currentUser ? {\n username: currentUser.username || currentUser.name || currentUser.email || 'Unknown User',\n name: currentUser.name,\n email: currentUser.email\n } : null,\n showLogin: !currentUser // Show login button if not authenticated\n };\n }\n\n /**\n * Handle going back to previous page\n */\n async handleActionGoBack(event, element) {\n event.preventDefault();\n\n // Try to go back in browser history\n if (window.history.length > 1) {\n window.history.back();\n } else {\n // Fallback to home page\n await this.handleActionGoHome(event, element);\n }\n }\n\n /**\n * Handle navigation to home page\n */\n async handleActionGoHome(event, element) {\n event.preventDefault();\n\n const app = this.getApp();\n if (app) {\n await app.navigateToDefault();\n } else {\n // Fallback navigation\n window.location.href = '/';\n }\n }\n\n /**\n * Handle login action\n */\n async handleActionLogin(event, element) {\n event.preventDefault();\n\n const app = this.getApp();\n\n // Try to navigate to login page\n if (app) {\n try {\n await app.showPage('login');\n } catch (error) {\n // If login page doesn't exist, try auth route\n try {\n await app.navigate('/login');\n } catch (navError) {\n // Fallback - emit login required event\n this.emit('login-required', {\n returnUrl: this.deniedPage?.route || window.location.pathname\n });\n\n // Show message if no handlers\n setTimeout(() => {\n app?.showInfo?.('Please contact your administrator for access.');\n }, 100);\n }\n }\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n // Set appropriate page title\n const pageName = this.deniedPage?.pageName || this.deniedPageName;\n if (pageName) {\n this.setMeta({\n title: `Access Denied - ${pageName}`\n });\n }\n\n // Log access denial for security monitoring\n console.warn('Access denied to page:', {\n page: this.deniedPage?.pageName || this.deniedPageName,\n route: this.deniedPage?.route,\n permissions: this.deniedPageOptions?.permissions,\n timestamp: new Date().toISOString()\n });\n }\n\n /**\n * Static helper to show access denied for a specific page\n */\n static showForPage(app, pageInstance) {\n const deniedPage = new DeniedPage();\n deniedPage.setDeniedPage(pageInstance);\n return app.showPage(deniedPage);\n }\n}\n\nexport default DeniedPage;\n","/**\n * NotFoundPage - 404 Not Found page for MOJO Framework\n * Displays when a user attempts to access a non-existent page or route\n */\n\nimport Page from '@core/Page.js';\n\nclass NotFoundPage extends Page {\n constructor(options = {}) {\n super({\n pageName: '404',\n route: '/404',\n title: '404 - Page Not Found',\n pageIcon: 'bi bi-search',\n template: `\n <div class=\"container mt-5\">\n <div class=\"row justify-content-center\">\n <div class=\"col-md-8 col-lg-6\">\n <div class=\"text-center mb-4\">\n <i class=\"bi bi-search text-muted\" style=\"font-size: 3rem;\"></i>\n <h2 class=\"mt-3 mb-2\">Page Not Found</h2>\n <p class=\"text-muted\">The page you're looking for doesn't exist.</p>\n </div>\n\n {{#path}}\n <div class=\"card border-0 shadow-sm mb-4\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-subtitle mb-2 text-muted\">Requested Path</h6>\n <code class=\"text-primary\">{{path}}</code>\n </div>\n </div>\n {{/path}}\n\n <div class=\"d-grid gap-2 d-md-flex justify-content-md-center\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"go-back\">\n <i class=\"bi bi-arrow-left me-1\"></i>\n Go Back\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"go-home\">\n <i class=\"bi bi-house me-1\"></i>\n Home\n </button>\n </div>\n </div>\n </div>\n </div>\n `,\n ...options\n });\n\n // Store the not found path\n this.path = null;\n }\n\n /**\n * Handle route parameters\n */\n async onParams(params = {}, query = {}) {\n await super.onParams(params, query);\n\n // Store path from params or query\n if (params.path) {\n this.path = params.path;\n }\n if (query.path) {\n this.path = query.path;\n }\n }\n\n /**\n * Set not found path\n */\n setInfo(path) {\n this.path = path || null;\n return this;\n }\n\n /**\n * Handle going back to previous page\n */\n async handleActionGoBack(event, _element) {\n event.preventDefault();\n\n // Try to go back in browser history\n if (window.history.length > 1) {\n window.history.back();\n } else {\n // Fallback to home page\n await this.handleActionGoHome(event, _element);\n }\n }\n\n /**\n * Handle navigation to home page\n */\n async handleActionGoHome(event, _element) {\n event.preventDefault();\n\n const app = this.getApp();\n if (app) {\n await app.navigateToDefault();\n } else {\n // Fallback navigation\n window.location.href = '/';\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n // Set appropriate page title\n if (this.path) {\n this.setMeta({\n title: `404 - ${this.path} Not Found`\n });\n }\n\n // Log 404 for analytics\n console.warn('404 Not Found:', {\n path: this.path,\n timestamp: new Date().toISOString()\n });\n }\n\n /**\n * Static helper to show 404 for a specific path\n */\n static showForPath(app, path) {\n const notFoundPage = new NotFoundPage();\n notFoundPage.setInfo(path);\n return notFoundPage.render(); // Just render, don't navigate\n }\n}\n\nexport default NotFoundPage;\n","/**\n * PortalApp - Complete portal application extending WebApp\n * Provides built-in navigation, sidebar, and content management\n * Clean, simple implementation that reuses WebApp and View logic\n */\n\nimport WebApp from '@core/WebApp.js';\nimport TopNav from '@core/views/navigation/TopNav.js';\nimport Sidebar from '@core/views/navigation/Sidebar.js';\nimport PageHeader from '@core/views/navigation/PageHeader.js';\nimport DeniedPage from '@core/pages/DeniedPage.js';\nimport TokenManager from '@core/services/TokenManager.js';\nimport {User} from '@core/models/User.js';\nimport {Group } from '@core/models/Group.js';\nimport {Member} from '@core/models/Member.js';\nimport NotFoundPage from '@core/pages/NotFoundPage.js';\nimport ToastService from '@core/services/ToastService.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\nexport default class PortalApp extends WebApp {\n constructor(config = {}) {\n // Pass core WebApp config through\n super(config);\n\n this.sidebarConfig = config.sidebar;\n // Portal-specific configuration (clean flat structure)\n // if (config.sidebar && config.sidebar.menus) {\n // this.sidebarConfig.menus = config.sidebar.menus;\n // this.sidebarConfig.groupSelectorMode = config.sidebar.groupSelectorMode || \"inline\";\n // if (config.sidebar.groupHeader) {\n // this.sidebarConfig.groupHeader = config.sidebar.groupHeader;\n // }\n // } else if (config.sidebar.menu) {\n // this.sidebarConfig.menu = config.sidebar.menu;\n // } else if (config.sidebar.items) {\n // this.sidebarConfig.menu = config.sidebar;\n // }\n\n this.topbarConfig = config.topbar || {};\n\n // Legacy support - topnav -> topbar\n if (config.topnav && !config.topbar) {\n this.topbarConfig = config.topnav;\n }\n\n // Page header configuration\n this.showPageHeader = config.showPageHeader || false;\n this.pageHeaderConfig = config.pageHeader || {};\n\n // Portal components\n this.sidebar = null;\n this.topbar = null;\n this.topnav = null; // Legacy reference\n this.pageHeader = null;\n this.tokenManager = new TokenManager();\n\n // Active group management\n this.activeGroup = null;\n // Portal state - Load from localStorage first, then fallback to config\n if (!this.isMobile()) {\n this.sidebarCollapsed = this.loadSidebarState() ??\n (this.sidebarConfig.defaultCollapsed || false);\n } else {\n this.sidebarCollapsed = this.sidebarConfig.defaultCollapsed || false;\n }\n this.setupPageContainer();\n\n this.toast = new ToastService();\n this.Dialog = Dialog;\n\n this.registerPage(\"denied\", DeniedPage);\n this.registerPage(\"404\", NotFoundPage);\n }\n\n /**\n * Override WebApp start to setup portal layout\n */\n async start() {\n // Call parent start (handles router, error handling, etc.)\n // Setup router\n await this.checkAuthStatus();\n\n this.events.on('auth:unauthorized', () => {\n this.tokenManager.clearTokens();\n this.rest.clearAuth();\n this.setActiveUser(null);\n return;\n });\n\n this.events.on('auth:logout', () => {\n this.tokenManager.clearTokens();\n this.rest.clearAuth();\n this.setActiveUser(null);\n return;\n });\n\n this.events.on(\"browser:focus\", () => {\n if (!this.activeUser) return;\n this.tokenManager.checkAndRefreshTokens(this);\n });\n\n this.events.on('portal:action', this.onPortalAction.bind(this));\n\n if (this.activeUser) {\n // Check and load active group after auth\n await this.checkActiveGroup();\n }\n\n\n await this.setupRouter();\n\n // Mark as started\n this.isStarted = true;\n\n // Emit app ready event\n this.events.emit('app:ready', { app: this });\n\n\n }\n\n async checkAuthStatus() {\n const tokenStatus = this.tokenManager.checkTokenStatus();\n\n // Handle logout scenarios\n if (tokenStatus.action === 'logout') {\n this.events.emit('auth:unauthorized', { app: this });\n return false;\n }\n\n // Handle refresh scenarios - attempt refresh if needed\n if (tokenStatus.action === 'refresh') {\n const refreshed = await this.tokenManager.checkAndRefreshTokens(this);\n if (!refreshed) {\n // If refresh failed, checkAndRefreshTokens already handled logout\n return false;\n }\n }\n\n // At this point we have a valid token\n const token = this.tokenManager.getTokenInstance();\n\n // If user already loaded, just start auto-refresh and return\n if (this.activeUser) {\n this.tokenManager.startAutoRefresh(this);\n return true;\n }\n\n // Load user data\n this.rest.setAuthToken(token.token);\n const user = new User({ id: token.getUserId() });\n const resp = await user.fetch();\n if (!resp.success) {\n this.tokenManager.clearTokens();\n this.events.emit('auth:unauthorized', { app: this, error: resp.error });\n return false;\n }\n\n this.setActiveUser(user);\n this.tokenManager.startAutoRefresh(this);\n return true;\n }\n\n /**\n * Check and load active group from storage\n */\n async checkActiveGroup() {\n // First check URL search params for group parameter\n const urlParams = new URLSearchParams(window.location.search);\n const urlGroupId = urlParams.get('group');\n\n // Determine which group ID to use: URL param takes priority\n const groupId = urlGroupId || this.loadActiveGroupId();\n\n if (groupId) {\n try {\n const group = new Group({ id: groupId });\n const resp = await group.fetch();\n if (!resp.success || !resp.data.status) {\n this.clearActiveGroup();\n console.warn('Failed to load active group:', resp.statusText);\n return;\n }\n\n this.activeGroup = group;\n // If we got the group from URL, save it as the new active group\n if (urlGroupId) {\n this.saveActiveGroupId(groupId);\n }\n\n if (this.activeUser) {\n this.activeUser.member = new Member();\n await this.activeUser.member.fetchForGroup(group.id);\n }\n\n // Emit event that group was loaded\n this.events.emit('group:loaded', { group: this.activeGroup });\n\n\n } catch (error) {\n console.warn('Failed to load active group:', error);\n // If URL group failed, try to clear it and fall back to stored group\n if (urlGroupId && !this.loadActiveGroupId()) {\n // URL group failed and no stored group, clear everything\n this.clearActiveGroupId();\n } else if (urlGroupId) {\n // URL group failed but we have a stored group, try that instead\n const storedGroupId = this.loadActiveGroupId();\n if (storedGroupId && storedGroupId !== urlGroupId) {\n try {\n const fallbackGroup = new Group({ id: storedGroupId });\n await fallbackGroup.fetch();\n this.activeGroup = fallbackGroup;\n this.events.emit('group:loaded', { group: this.activeGroup });\n\n } catch (fallbackError) {\n console.warn('Fallback to stored group also failed:', fallbackError);\n this.clearActiveGroupId();\n }\n }\n }\n }\n }\n }\n\n\n /**\n * Set the active group\n */\n async setActiveGroup(group) {\n const previousGroup = this.activeGroup;\n this.activeGroup = group;\n\n // Save to storage\n if (group && group.get('id')) {\n this.saveActiveGroupId(group.get('id'));\n } else {\n this.clearActiveGroupId();\n }\n\n if (this.activeUser) {\n this.activeUser.member = new Member();\n await this.activeUser.member.fetchForGroup(group.id);\n }\n\n // Emit event\n this.events.emit('group:changed', {\n group,\n previousGroup,\n app: this\n });\n\n const page = this.getCurrentPage();\n if (page) {\n page.onGroupChange(group);\n }\n\n this.router.updateUrl({group:group.id}, { replace: true });\n\n return this;\n }\n\n /**\n * Get the active group\n */\n getActiveGroup() {\n return this.activeGroup;\n }\n\n /**\n * Clear the active group\n */\n async clearActiveGroup() {\n const previousGroup = this.activeGroup;\n this.activeGroup = null;\n this.clearActiveGroupId();\n // Emit event\n this.events.emit('group:cleared', {\n previousGroup,\n app: this\n });\n return this;\n }\n\n /**\n * Save active group ID to localStorage\n */\n saveActiveGroupId(groupId) {\n try {\n const key = this.getActiveGroupStorageKey();\n localStorage.setItem(key, groupId.toString());\n } catch (error) {\n console.warn('Failed to save active group ID:', error);\n }\n }\n\n /**\n * Load active group ID from localStorage\n */\n loadActiveGroupId() {\n try {\n const key = this.getActiveGroupStorageKey();\n return localStorage.getItem(key);\n } catch (error) {\n console.warn('Failed to load active group ID:', error);\n return null;\n }\n }\n\n /**\n * Clear active group ID from localStorage\n */\n clearActiveGroupId() {\n try {\n const key = this.getActiveGroupStorageKey();\n localStorage.removeItem(key);\n } catch (error) {\n console.warn('Failed to clear active group ID:', error);\n }\n }\n\n /**\n * Get storage key for active group ID\n */\n getActiveGroupStorageKey() {\n return `active_group_id`;\n }\n\n /**\n * Set portal profile to localStorage\n */\n setPortalProfile(profile) {\n try {\n localStorage.setItem('portal_profile', profile);\n } catch (error) {\n console.warn('Failed to save portal profile:', error);\n }\n }\n\n /**\n * Check if user needs to select a group\n */\n needsGroupSelection() {\n return !this.activeGroup;\n }\n\n /**\n * Setup layout based on configuration\n */\n setupPageContainer() {\n const container = typeof this.container === 'string'\n ? document.querySelector(this.container)\n : this.container;\n\n if (!container) {\n throw new Error(`Portal container not found: ${this.container}`);\n }\n\n // Create clean portal layout\n const showSidebar = this.sidebarConfig && Object.keys(this.sidebarConfig).length > 0;\n const showTopbar = this.topbarConfig && Object.keys(this.topbarConfig).length > 0;\n\n // If page header is enabled, wrap content in two containers\n const contentMarkup = this.showPageHeader ? `\n <div class=\"portal-content\" id=\"portal-content\">\n <div id=\"page-header\"></div>\n <div id=\"page-container\">\n <!-- Pages render here -->\n </div>\n </div>\n ` : `\n <div class=\"portal-content\" id=\"page-container\">\n <!-- Pages render here -->\n </div>\n `;\n\n container.innerHTML = `\n <div class=\"portal-layout hide-sidebar\">\n ${showSidebar ? '<div id=\"portal-sidebar\"></div>' : ''}\n <div class=\"portal-body\">\n ${showTopbar ? '<div id=\"portal-topnav\"></div>' : ''}\n ${contentMarkup}\n </div>\n </div>\n `;\n\n // Set page container for WebApp\n this.pageContainer = '#page-container';\n\n // Add portal CSS classes and apply saved state\n container.classList.add('portal-container');\n\n // Setup page container\n this.setupPortalComponents();\n\n // Apply the saved sidebar state\n this.applySidebarState(container);\n }\n\n /**\n * Setup portal components\n */\n async setupPortalComponents() {\n await this.setupSidebar();\n await this.setupTopbar();\n await this.setupPageHeader();\n this.setupPortalEvents();\n }\n\n /**\n * Setup sidebar component\n */\n async setupSidebar() {\n if (!this.sidebarConfig || Object.keys(this.sidebarConfig).length === 0) return;\n\n this.sidebar = new Sidebar({\n containerId: 'portal-sidebar',\n ...this.sidebarConfig\n });\n\n await this.sidebar.render();\n }\n\n /**\n * Setup topbar component\n */\n async setupTopbar() {\n if (!this.topbarConfig || Object.keys(this.topbarConfig).length === 0) return;\n\n // Map config to TopNav format\n this.topbar = new TopNav({\n containerId: \"portal-topnav\",\n brandText: this.topbarConfig.brand || this.brand || this.title,\n brandRoute: this.topbarConfig.brandRoute || '/',\n brandIcon: this.topbarConfig.brandIcon || this.brandIcon,\n navItems: this.topbarConfig.leftItems || [],\n rightItems: this.topbarConfig.rightItems || [],\n displayMode: this.topbarConfig.displayMode || 'both',\n showSidebarToggle: this.topbarConfig.showSidebarToggle || false,\n ...this.topbarConfig\n });\n\n await this.topbar.render();\n\n // Legacy support\n this.topnav = this.topbar;\n }\n\n /**\n * Setup page header component\n */\n async setupPageHeader() {\n if (!this.showPageHeader) return;\n\n this.pageHeader = new PageHeader({\n containerId: 'page-header',\n style: this.pageHeaderConfig.style || 'default',\n showIcon: this.pageHeaderConfig.showIcon !== false,\n showDescription: this.pageHeaderConfig.showDescription !== false,\n showBreadcrumbs: this.pageHeaderConfig.showBreadcrumbs || false,\n ...this.pageHeaderConfig\n });\n\n // Render into the portal-content container\n const headerContainer = document.getElementById('page-header');\n if (headerContainer) {\n await this.pageHeader.render(true, headerContainer);\n }\n }\n\n /**\n * Setup portal event handling\n */\n setupPortalEvents() {\n // Handle sidebar toggle via event delegation\n document.addEventListener('click', (event) => {\n if (event.target.closest('[data-action=\"toggle-sidebar\"]')) {\n event.preventDefault();\n this.toggleSidebar();\n }\n });\n\n // Handle responsive changes\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n this.handleResponsive();\n });\n resizeObserver.observe(document.body);\n this._resizeObserver = resizeObserver;\n } else {\n // Fallback for older browsers\n this._resizeHandler = () => this.handleResponsive();\n window.addEventListener('resize', this._resizeHandler);\n }\n\n // Initial responsive setup\n this.handleResponsive();\n }\n\n /**\n * Toggle sidebar state\n */\n toggleSidebar() {\n if (!this.sidebar) return;\n\n const container = document.querySelector('.portal-container');\n const isMobile = this.isMobile();\n\n if (isMobile) {\n container.classList.toggle('hide-sidebar');\n } else {\n container.classList.toggle('collapse-sidebar');\n this.sidebarCollapsed = !this.sidebarCollapsed;\n\n // Save the new state\n this.saveSidebarState(this.sidebarCollapsed);\n }\n\n this.events.emit('sidebar:toggled', {\n collapsed: this.sidebarCollapsed,\n mobile: isMobile\n });\n }\n\n /**\n * Handle responsive layout\n */\n handleResponsive() {\n const container = document.querySelector('.portal-container');\n if (!container) return;\n const isMobile = this.isMobile();\n\n if (isMobile) {\n container.classList.add('mobile-layout');\n if (!container.classList.contains('hide-sidebar')) {\n container.classList.add('hide-sidebar');\n }\n } else {\n container.classList.remove('mobile-layout', 'hide-sidebar');\n }\n\n this.events.emit('responsive:changed', { mobile: isMobile });\n }\n\n getPortalContainer() {\n return document.querySelector('.portal-container');\n }\n\n isMobile() {\n return window.innerWidth < 768;\n }\n\n hasMobileLayout() {\n return this.getPortalContainer().classList.contains('mobile-layout');\n }\n\n /**\n * Override showPage to update navigation\n */\n async showPage(page, query = {}, params = {}, options = {}) {\n const result = await super.showPage(page, query, params, options);\n\n if (this.hasMobileLayout()) {\n this.getPortalContainer().classList.add('hide-sidebar');\n }\n\n if (this.currentPage) {\n this.updateNavigation(this.currentPage);\n }\n\n return result;\n }\n\n /**\n * Update navigation active states\n */\n updateNavigation(page) {\n // Update sidebar active state\n if (this.sidebar && this.sidebar.setActivePage) {\n this.sidebar.setActivePage(page.route);\n }\n\n // Update topbar active state\n if (this.topbar && this.topbar.setActivePage) {\n this.topbar.setActivePage(page.route);\n }\n\n // Update page header\n if (this.pageHeader) {\n this.pageHeader.setPage(page);\n }\n\n this.events.emit('portal:page-changed', { page });\n }\n\n /**\n * Set active user\n */\n setActiveUser(user) {\n this.activeUser = user;\n\n if (this.topbar) {\n this.topbar.setUser(user);\n }\n\n\n this.events.emit('portal:user-changed', { user });\n }\n\n /**\n * Get the active user (for backward compatibility)\n */\n getActiveUser() {\n return this.activeUser;\n }\n\n /**\n * Save sidebar state to localStorage\n */\n saveSidebarState(collapsed) {\n try {\n const key = this.getSidebarStorageKey();\n localStorage.setItem(key, JSON.stringify(collapsed));\n } catch (error) {\n console.warn('Failed to save sidebar state:', error);\n }\n }\n\n /**\n * Load sidebar state from localStorage\n */\n loadSidebarState() {\n try {\n const key = this.getSidebarStorageKey();\n const saved = localStorage.getItem(key);\n return saved !== null ? JSON.parse(saved) : null;\n } catch (error) {\n console.warn('Failed to load sidebar state:', error);\n return null;\n }\n }\n\n /**\n * Get storage key for sidebar state (allows multiple apps on same domain)\n */\n getSidebarStorageKey() {\n // Use app title/name to create unique key\n const appKey = this.title ? this.title.replace(/\\s+/g, '_').toLowerCase() : 'portal_app';\n return `${appKey}_sidebar_collapsed`;\n }\n\n /**\n * Apply saved sidebar state to the UI\n */\n applySidebarState(container = null) {\n if (!container) {\n container = document.querySelector('.portal-container');\n }\n\n if (!container) return;\n\n if (this.sidebarCollapsed) {\n container.classList.add('collapse-sidebar');\n } else {\n container.classList.remove('collapse-sidebar');\n }\n }\n\n /**\n * Clear saved sidebar state\n */\n clearSidebarState() {\n try {\n const key = this.getSidebarStorageKey();\n localStorage.removeItem(key);\n } catch (error) {\n console.warn('Failed to clear sidebar state:', error);\n }\n }\n\n async changePassword() {\n const data = await this.showForm({\n title: \"Change Password\",\n fields: [\n {\n name: 'current_password', type: 'password',\n label: 'Current Password', required: true,\n showToggle: true, // default, can omit\n strengthMeter: true,\n capsLockWarning: true,\n\n },\n {\n name: 'new_password', type: 'password', label: 'New Password', required: true,\n showToggle: true,\n passwordUsage: 'new', // sets autocomplete to 'new-password'\n\n strengthMeter: true,\n capsLockWarning: true,\n attributes: {\n // optional, override autocomplete if needed\n autocomplete: 'new-password'\n }\n },\n {\n name: 'confirm_password', type: 'password', label: 'Confirm Password', required: true,\n showToggle: true,\n passwordUsage: 'new', // sets autocomplete to 'new-password'\n\n strengthMeter: true,\n capsLockWarning: true,\n attributes: {\n // optional, override autocomplete if needed\n // autocomplete: 'new-password'\n }\n }\n ],\n submitLabel: 'Change Password'\n });\n if (data) {\n if (data.new_password === data.confirm_password) {\n // Perform password change logic here\n const resp = await this.activeUser.save(data);\n if (resp.status === 200) {\n this.toast.success('Password changed successfully');\n } else {\n this.toast.error('Failed to change password');\n }\n } else {\n this.toast.error('Passwords do not match');\n }\n }\n }\n\n onPortalAction(action) {\n switch (action.action) {\n case 'logout':\n this.tokenManager.clearTokens();\n this.rest.clearAuth();\n this.setActiveUser(null);\n break;\n case 'profile':\n this.showProfile();\n break;\n case 'change-password':\n this.changePassword();\n break;\n default:\n console.warn(`Unknown portal action: ${action}`);\n }\n }\n\n async showProfile() {\n if (!this.activeUser) {\n this.showError(\"No user is currently logged in\");\n return;\n }\n\n try {\n\n\n // Show profile edit form with automatic model saving\n const result = await Dialog.showModelForm({\n title: 'Edit Profile',\n size: 'lg',\n fileHandling: 'base64',\n model: this.activeUser,\n fields: [\n // Profile Header\n {\n type: 'header',\n text: 'Profile Information',\n level: 4,\n class: 'text-primary mb-3'\n },\n\n // Avatar and Basic Info\n {\n type: 'group',\n columns: { xs: 12, md: 4 },\n title: 'Avatar',\n fields: [\n {\n type: 'image',\n name: 'avatar',\n size: 'lg',\n imageSize: { width: 200, height: 200 },\n placeholder: 'Upload your avatar',\n help: 'Square images work best'\n }\n ]\n },\n\n // Profile Details\n {\n type: 'group',\n columns: { xs: 12, md: 8 },\n title: 'Details',\n fields: [\n {\n type: 'text',\n name: 'display_name',\n label: 'Display Name',\n required: true,\n columns: 12,\n placeholder: 'Enter first name'\n },\n {\n type: 'email',\n name: 'email',\n label: 'Email Address',\n required: true,\n columns: 8,\n placeholder: 'your.email@example.com'\n },\n {\n type: 'tel',\n name: 'phone_number',\n label: 'Phone Number',\n columns: 4,\n placeholder: '(555) 123-4567'\n },\n ]\n },\n\n // Account Settings\n {\n type: 'group',\n columns: 12,\n title: 'Account Settings',\n class: \"pt-3\",\n fields: [\n {\n type: 'select',\n name: 'timezone',\n label: 'Timezone',\n columns: 6,\n options: [\n { value: 'America/New_York', text: 'Eastern Time' },\n { value: 'America/Chicago', text: 'Central Time' },\n { value: 'America/Denver', text: 'Mountain Time' },\n { value: 'America/Los_Angeles', text: 'Pacific Time' },\n { value: 'UTC', text: 'UTC' }\n ]\n },\n {\n type: 'select',\n name: 'language',\n label: 'Language',\n columns: 6,\n options: [\n { value: 'en', text: 'English' },\n { value: 'es', text: 'Spanish' },\n { value: 'fr', text: 'French' },\n { value: 'de', text: 'German' }\n ]\n },\n {\n type: 'switch',\n name: 'email_notifications',\n label: 'Email Notifications',\n columns: 4\n },\n {\n type: 'switch',\n name: 'two_factor_enabled',\n label: 'Two-Factor Authentication',\n columns: 4\n },\n {\n type: 'switch',\n name: 'profile_public',\n label: 'Public Profile',\n columns: 4\n }\n ]\n }\n ],\n submitText: 'Save Profile',\n cancelText: 'Cancel'\n });\n\n if (result && result.success) {\n\n\n // Update active user with new data from model\n // (model should already be updated by save operation)\n\n // Show success message\n this.showSuccess('Profile updated successfully!');\n } else if (result && !result.success) {\n // Error case - already handled by Dialog.showForm\n\n }\n\n } catch (error) {\n console.error('Error showing profile form:', error);\n this.showError('Failed to load profile form');\n }\n }\n\n /**\n * Clean up portal resources\n */\n async destroy() {\n // Clean up event listeners\n // Clear active group\n this.activeGroup = null;\n\n // Clean up portal resources\n if (this._resizeObserver) {\n this._resizeObserver.disconnect();\n }\n if (this._resizeHandler) {\n window.removeEventListener('resize', this._resizeHandler);\n }\n\n // Destroy components using View lifecycle\n if (this.topbar) {\n await this.topbar.destroy();\n this.topbar = null;\n this.topnav = null;\n }\n\n if (this.sidebar) {\n await this.sidebar.destroy();\n this.sidebar = null;\n }\n\n // Call parent destroy\n await super.destroy();\n }\n\n /**\n * Static factory method\n */\n static create(config = {}) {\n return new PortalApp(config);\n }\n}\n","import FormView from './FormView.js';\nimport Page from '@core/Page.js';\n\nexport default class FormPage extends Page {\n constructor(options = {}) {\n super({\n title: 'Form Page',\n description: 'A page for submitting forms',\n icon: 'form',\n fields: [],\n template: '<div data-container=\"form-view-container\"></div>',\n className: \"form-page container-sm\",\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n await this.recreateFormView();\n }\n\n async onEnter() {\n await super.onEnter();\n if (this.formView) {\n // Recreate formView to ensure clean slate with new model\n await this.recreateFormView();\n }\n }\n\n async onGroupChange(group) {\n if (this.formView) {\n // Recreate formView to ensure clean slate with new model\n await this.recreateFormView();\n }\n }\n\n async getModel() {\n if (this.model) {\n return this.model;\n } else if (this.getApp().activeGroup) {\n return this.getApp().activeGroup;\n }\n return null;\n }\n\n async recreateFormView() {\n // Destroy old formView\n if (this.formView) {\n await this.formView.destroy();\n this.removeChild(this.formView);\n }\n\n // Create new formView with current model\n this.formView = new FormView({\n containerId: 'form-view-container',\n fields: this.options.fields,\n autosaveModelField: true\n });\n this.addChild(this.formView);\n\n const model = await this.getModel();\n if (model) {\n this.formView.setModel(model);\n }\n }\n}\n","/**\n * MustacheFormatter - Mustache wrapper for MOJO Framework\n * \n * This is now a thin wrapper around Mustache.js since pipe formatting\n * is handled directly in Model.get() and View.get() via MOJOUtils.\n * \n * The wrapper is maintained for backward compatibility and to provide\n * a consistent API for template rendering throughout the framework.\n */\n\nimport mustache from './mustache.js';\nimport dataFormatter from './DataFormatter.js';\n\nclass MustacheFormatter {\n constructor() {\n this.formatter = dataFormatter;\n this.compiledTemplates = new Map();\n }\n\n /**\n * Render template with data\n * Pipes are now handled by Model.get() and View.get() automatically\n * \n * @param {string} template - Mustache template\n * @param {object} data - Data to render (View, Model, or plain object)\n * @param {object} partials - Mustache partials\n * @returns {string} Rendered template\n */\n render(template, data, partials = {}) {\n // Simply pass through to Mustache\n // If data has a get() method, Mustache will use it automatically\n return mustache.render(template, data, partials);\n }\n\n /**\n * Compile template for reuse\n * @param {string} template - Template to compile\n * @returns {object} Compiled template tokens\n */\n compile(template) {\n const compiled = mustache.parse(template);\n this.compiledTemplates.set(template, compiled);\n return compiled;\n }\n\n /**\n * Render with compiled template\n * @param {object} compiled - Compiled template tokens\n * @param {object} data - Data to render\n * @param {object} partials - Mustache partials\n * @returns {string} Rendered template\n */\n renderCompiled(compiled, data, partials = {}) {\n return mustache.render(compiled, data, partials);\n }\n\n /**\n * Clear compiled template cache\n */\n clearCache() {\n this.compiledTemplates.clear();\n mustache.clearCache();\n }\n\n /**\n * Process and cache a template\n * @param {string} key - Cache key\n * @param {string} template - Template to cache\n * @returns {object} Cached template info\n */\n cache(key, template) {\n const compiled = this.compile(template);\n return { key, template, compiled };\n }\n\n /**\n * Get cached template\n * @param {string} key - Cache key\n * @returns {object|null} Cached template info or null\n */\n getCached(key) {\n for (const [template, compiled] of this.compiledTemplates) {\n if (template === key || compiled === key) {\n return { key, template, compiled };\n }\n }\n return null;\n }\n\n /**\n * Register a custom formatter with DataFormatter\n * @param {string} name - Formatter name\n * @param {function} formatter - Formatter function\n * @returns {MustacheFormatter} This instance for chaining\n */\n registerFormatter(name, formatter) {\n this.formatter.register(name, formatter);\n return this;\n }\n\n /**\n * Check if a string contains pipe syntax\n * @param {string} template - Template string to check\n * @returns {boolean} True if contains pipes\n */\n hasPipes(template) {\n return /\\{\\{[{]?[^}|]+\\|[^}]+\\}[}]?\\}/.test(template);\n }\n\n /**\n * Pre-process data with pipe formatters\n * This is now handled automatically by get() methods, but kept for backward compatibility\n * \n * @param {object} data - Data object\n * @param {object} pipes - Object mapping keys to pipe strings\n * @returns {object} Processed data\n */\n processData(data, pipes) {\n const processed = { ...data };\n \n for (const [key, pipeString] of Object.entries(pipes)) {\n // If data has a get method, use it (which will handle pipes)\n if (data && typeof data.get === 'function') {\n processed[key] = data.get(`${key}|${pipeString}`);\n } else {\n // For plain objects, apply formatter directly\n const value = this.getValueFromPath(data, key);\n processed[key] = this.formatter.pipe(value, pipeString);\n }\n }\n \n return processed;\n }\n\n /**\n * Get value from object using dot notation path\n * Kept for backward compatibility, but MOJOUtils.getContextData is preferred\n * \n * @param {object} obj - Source object\n * @param {string} path - Dot notation path\n * @returns {*} Value at path\n */\n getValueFromPath(obj, path) {\n if (!obj || !path) return undefined;\n \n // If obj has a get method, use it\n if (obj && typeof obj.get === 'function') {\n return obj.get(path);\n }\n \n // Otherwise use standard property navigation\n const keys = path.split('.');\n let current = obj;\n \n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n \n // Handle array index\n if (!isNaN(key) && Array.isArray(current)) {\n current = current[parseInt(key)];\n } else {\n current = current[key];\n }\n }\n \n return current;\n }\n\n /**\n * Process template to handle pipe formatters\n * @deprecated Pipes are now handled by get() methods automatically\n * @param {string} template - Original template\n * @param {object} data - Original data\n * @returns {object} {template: processedTemplate, data: processedData}\n */\n processTemplate(template, data) {\n // For backward compatibility, just return as-is\n // Pipes will be handled by get() methods during Mustache rendering\n return { template, data };\n }\n}\n\n// Create singleton instance\nconst mustacheFormatter = new MustacheFormatter();\n\n// Export both class and instance\nexport { MustacheFormatter };\nexport default mustacheFormatter;","/**\n * MOJO Framework - Core Entry (2.1.0)\n */\n\n// Bundle core CSS\nimport '@core/css/core.css';\nimport '@core/css/portal.css';\nimport '@core/css/table.css';\nimport '@core/css/toast.css';\nimport '@core/css/chat.css';\n\nimport ConsoleSilencer from '@core/utils/ConsoleSilencer.js';\n// Reduce console noise globally: errors only by default (suppress logs, info, and warnings)\nConsoleSilencer.install({ level: 'warn' });\n\n// Version info\nexport {\n VERSION_INFO,\n VERSION,\n VERSION_MAJOR,\n VERSION_MINOR,\n VERSION_REVISION,\n BUILD_TIME\n} from './version.js';\n\n// Core runtime\nexport { default as View } from '@core/View.js';\nexport { default as Page } from '@core/Page.js';\nexport { default as Router } from '@core/Router.js';\nexport { default as Model } from '@core/Model.js';\nexport { default as Collection } from '@core/Collection.js';\nexport { default as Rest } from '@core/Rest.js';\n\n// Core Models - re-export everything from models/\nexport * from '@core/models/AWS.js';\nexport * from '@core/models/Email.js';\nexport * from '@core/models/Files.js';\nexport * from '@core/models/Group.js';\nexport * from '@core/models/Incident.js';\nexport * from '@core/models/Job.js';\nexport * from '@core/models/JobRunner.js';\nexport * from '@core/models/Log.js';\nexport * from '@core/models/Member.js';\nexport * from '@core/models/Metrics.js';\nexport * from '@core/models/Push.js';\nexport * from '@core/models/System.js';\nexport * from '@core/models/Tickets.js';\nexport * from '@core/models/User.js';\n\n// App classes\nexport { default as WebApp } from '@core/WebApp.js';\nexport { default as PortalApp } from '@core/PortalApp.js';\n\n// UI helper\nexport { default as Dialog } from '@core/views/feedback/Dialog.js';\n\n// Selected views (curated for tree-shaking)\nexport { default as TableView } from '@core/views/table/TableView.js';\nexport { default as TableRow } from '@core/views/table/TableRow.js';\nexport { default as TablePage } from '@core/pages/TablePage.js';\nexport { default as ListView } from '@core/views/list/ListView.js';\nexport { default as ListViewItem } from '@core/views/list/ListViewItem.js';\nexport { default as TopNav } from '@core/views/navigation/TopNav.js';\nexport { default as Sidebar } from '@core/views/navigation/Sidebar.js';\nexport { default as TabView } from '@core/views/navigation/TabView.js';\nexport { default as SimpleSearchView } from '@core/views/navigation/SimpleSearchView.js';\nexport { default as DataView } from '@core/views/data/DataView.js';\nexport { default as FormView } from '@core/forms/FormView.js';\nexport { default as FormPage } from '@core/forms/FormPage.js';\nexport { default as FilePreviewView } from '@core/views/data/FilePreviewView.js';\nexport { default as ChatView } from '@core/views/chat/ChatView.js';\nexport { default as ChatMessageView } from '@core/views/chat/ChatMessageView.js';\nexport { default as ChatInputView } from '@core/views/chat/ChatInputView.js';\n\n// Services, utils, mixins\nexport { default as FileUpload } from '@core/services/FileUpload.js';\nexport { default as applyFileDropMixin } from '@core/mixins/FileDropMixin.js';\nexport { default as TokenManager } from '@core/services/TokenManager.js';\nexport { default as ToastService } from '@core/services/ToastService.js';\nexport { default as WebSocketClient } from '@core/services/WebSocketClient.js';\nexport { default as EventDelegate } from '@core/mixins/EventDelegate.js';\nexport { default as EventBus } from '@core/utils/EventBus.js';\nexport { default as dataFormatter } from '@core/utils/DataFormatter.js';\nexport { default as MustacheFormatter } from '@core/utils/MustacheFormatter.js';\nexport { default as MOJOUtils, DataWrapper } from '@core/utils/MOJOUtils.js';\nexport { default as ConsoleSilencer } from '@core/utils/ConsoleSilencer.js';\nexport { installConsoleSilencer } from '@core/utils/ConsoleSilencer.js';\nexport { default as DjangoLookups, parseFilterKey, formatFilterDisplay, LOOKUPS } from '@core/utils/DjangoLookups.js';\n\n// Additional views\nexport { default as ProgressView } from '@core/views/feedback/ProgressView.js';\nexport { default as ContextMenu } from '@core/views/feedback/ContextMenu.js';\n\n// Names\nexport const FRAMEWORK_NAME = 'MOJO';\nexport const PACKAGE_NAME = 'web-mojo';\n\nexport default {\n FRAMEWORK_NAME,\n PACKAGE_NAME,\n};\n"],"names":["LEVELS","Object","freeze","silent","error","warn","info","log","debug","trace","all","isDev","url","document","require","pathToFileURL","__filename","href","_documentCurrentScript","tagName","toUpperCase","src","URL","baseURI","__vite_import_meta_env__","globalThis","__DEV__","process","env","NODE_ENV","isBrowser","window","GLOBAL","global","ORIGINAL_CONSOLE","console","ORIGINALS","INSTALLED","CURRENT_LEVEL","parseLevel","level","min","max","Math","key","toLowerCase","prototype","hasOwnProperty","call","makeWrapper","methodName","methodLevel","original","args","apply","ConsoleSilencer","install","options","this","setLevel","persist","explicitLevel","explicit","urlLevel","location","search","params","URLSearchParams","keys","k","v","get","parsed","getUrlLogLevel","storedLevel","localStorage","getItem","getStoredLogLevel","determineInitialLevel","patched","methodLevels","dir","table","group","groupCollapsed","groupEnd","time","timeEnd","timeLog","name","assert","condition","makeAssertWrapper","buildPatchedConsole","MOJOConsoleSilencer","uninstall","levelNumberOrName","entries","find","num","setItem","removeItem","storeLogLevel","getLevel","getLevelName","entry","criticalOnly","errorsOnly","verbose","allowAll","withTemporaryLevel","fn","prev","GroupSearchView","SimpleSearchView","constructor","super","className","trim","showKind","parentField","kindField","treeData","flattenedItems","showLines","buildTreeHierarchy","items","length","itemsById","Map","forEach","item","has","id","set","children","hasChildren","parentObj","rootItems","treeItem","itemId","originalItem","i","parentId","parent","push","calculateLevels","nodes","node","sort","a","b","localeCompare","flattenTree","result","ancestorLastFlags","index","_isLastChild","_ancestorLastFlags","allAncestorsLast","every","flag","_isLastDescendant","newFlags","computeVerticalLines","flatList","_continueVertical","s","foundSibling","j","futureItem","updateFilteredItems","collection","filteredItems","toJSON","updateResultsView","getDefaultItemTemplate","processItemTemplate","content","itemTemplate","replace","match","prop","getNestedValue","lineSegments","getRootItems","getNodeChildren","nodeId","findNode","targetId","found","Sidebar","View","menus","activeMenuName","currentRoute","showToggle","isCollapsed","sidebarTheme","theme","customView","groupHeader","groupSelectorMode","groupSelectorDialog","addClass","initializeMenus","setupRouteListeners","autoCollapseMobile","setupResponsiveBehavior","onInit","app","getApp","router","currentPath","getCurrentPath","autoSwitchToMenuForRoute","initializeTooltips","searchView","noAppend","showExitButton","headerText","containerId","Collection","GroupList","addChild","on","evt","setActiveGroup","model","hideGroupSearch","showGroupSearch","showGroupSearchDialog","setClass","showSearch","render","hide","onActionShowGroupSearch","onActionSelectGroupParent","activeGroup","Dialog","confirm","showLoading","Group","fetch","hideLoading","searchFields","searchPlaceholder","headerIcon","maxHeight","innerHeight","autoExpandRoot","autoExpandAll","indentSize","body","size","header","noBodyPadding","scrollable","buttons","closeButton","destroy","show","route","menuName","menuConfig","groupKind","menuContainsRoute","_setActiveMenu","clearAllActiveStates","setActiveItemByRoute","emit","config","sidebar","active","child","normalizeRoute","r","decoded","decodeURIComponent","startsWith","targetRoute","itemRoute","routesMatch","activeMenuItem","childRoute","doRoutesMatch","getTemplate","getSearchTemplate","getMenuTemplate","getPartials","getGroupHeader","addMenu","footer","data","getCurrentMenuConfig","setActiveMenu","lastGroupMenu","getGroupMenu","targetMenu","anyGroupMenu","_","kind","_groupKindMatches","Array","isArray","includes","showMenuForGroup","getMenuConfig","updateMenu","updates","menu","assign","removeMenu","delete","remainingMenus","from","onBeforeRender","currentMenu","subData","version","user","activeUser","renderTemplateString","processNavItems","onAfterRender","isCollapsedState","setTimeout","destroyTooltips","setCustomView","view","removeChild","clearCustomView","updateRouteWithGroup","normalizedRoute","substring","separator","map","divider","isDivider","spacer","isSpacer","processedItem","permissions","hasPermission","requiresGroupKind","isLabel","page","baseRoute","processedChild","filter","isItemActive","updateActiveItem","handleActionToggleSubmenu","event","element","arrow","querySelector","classList","toggle","handleActionToggleSidebar","toggleSidebar","onActionShowGroupMenu","action","el","onActionDefault","findAndExecuteHandler","handler","getMenuNames","hasMenu","clearMenus","clear","setMenuData","getMenuData","events","onRouteChanged","preferredMenu","sidebarMenu","fallbackMenu","portalContainer","hideAllTooltips","isCurrentlyCollapsed","contains","remove","add","setSidebarState","state","querySelectorAll","link","navText","textContent","tooltipText","setAttribute","bootstrap","Tooltip","getAttribute","customClass","tooltipOptions","placement","container","trigger","delay","fallbackPlacements","trimmedClass","tooltip","_tooltipInstance","addEventListener","addTooltipHideListeners","removeTooltipHideListeners","tooltipInstance","getInstance","dispose","removeAttribute","getSidebarState","setToggleEnabled","enabled","_tooltipScrollHandler","passive","_tooltipRouteHandler","_tooltipBlurHandler","_tooltipEscapeHandler","e","removeEventListener","onBeforeDestroy","checkMobile","isMobile","innerWidth","createDefault","createMinimal","setSidebarTheme","removeClass","collapse","expand","pulseToggle","toggleButton","removePulse","once","addSimpleMenuItem","text","icon","setSimpleMenu","PageHeader","style","showIcon","showDescription","showBreadcrumbs","currentPage","getMinimalTemplate","getBreadcrumbTemplate","getDefaultTemplate","hasPage","title","displayName","pageName","pageIcon","pageDescription","description","headerActions","pageTitle","breadcrumbs","actions","hasActions","setPage","getPage","onHeaderAction","DeniedPage","Page","template","deniedPage","deniedPageOptions","onParams","query","pageOptions","deniedPageName","setDeniedPage","pageInstance","getViewData","currentUser","getCurrentUser","deniedPageInfo","requiredPermissions","username","email","showLogin","handleActionGoBack","preventDefault","history","back","handleActionGoHome","navigateToDefault","handleActionLogin","showPage","navigate","navError","returnUrl","pathname","showInfo","onEnter","setMeta","timestamp","Date","toISOString","showForPage","NotFoundPage","path","setInfo","_element","showForPath","notFoundPage","PortalApp","WebApp","sidebarConfig","topbarConfig","topbar","topnav","showPageHeader","pageHeaderConfig","pageHeader","tokenManager","TokenManager","sidebarCollapsed","defaultCollapsed","loadSidebarState","setupPageContainer","toast","ToastService","registerPage","start","checkAuthStatus","clearTokens","rest","clearAuth","setActiveUser","checkAndRefreshTokens","onPortalAction","bind","checkActiveGroup","setupRouter","isStarted","tokenStatus","checkTokenStatus","token","getTokenInstance","startAutoRefresh","setAuthToken","User","getUserId","resp","success","urlGroupId","groupId","loadActiveGroupId","status","clearActiveGroup","statusText","saveActiveGroupId","member","Member","fetchForGroup","clearActiveGroupId","storedGroupId","fallbackGroup","fallbackError","previousGroup","getCurrentPage","onGroupChange","updateUrl","getActiveGroup","getActiveGroupStorageKey","toString","setPortalProfile","profile","needsGroupSelection","Error","showSidebar","showTopbar","contentMarkup","innerHTML","pageContainer","setupPortalComponents","applySidebarState","setupSidebar","setupTopbar","setupPageHeader","setupPortalEvents","TopNav","brandText","brand","brandRoute","brandIcon","navItems","leftItems","rightItems","displayMode","showSidebarToggle","headerContainer","getElementById","target","closest","ResizeObserver","resizeObserver","handleResponsive","observe","_resizeObserver","_resizeHandler","saveSidebarState","collapsed","mobile","getPortalContainer","hasMobileLayout","updateNavigation","setActivePage","setUser","getActiveUser","getSidebarStorageKey","JSON","stringify","saved","parse","clearSidebarState","changePassword","showForm","fields","type","label","required","strengthMeter","capsLockWarning","passwordUsage","attributes","autocomplete","submitLabel","new_password","confirm_password","save","showProfile","showModelForm","fileHandling","class","columns","xs","md","imageSize","width","height","placeholder","help","value","submitText","cancelText","showSuccess","showError","disconnect","create","FormPage","recreateFormView","formView","getModel","FormView","autosaveModelField","setModel","mustacheFormatter","formatter","dataFormatter","compiledTemplates","partials","mustache","compile","compiled","renderCompiled","clearCache","cache","getCached","registerFormatter","register","hasPipes","test","processData","pipes","processed","pipeString","getValueFromPath","pipe","obj","split","current","isNaN","parseInt","processTemplate","FRAMEWORK_NAME","PACKAGE_NAME"],"mappings":"2vBAiCMA,EAASC,OAAOC,OAAO,CAC3BC,OAAQ,EACRC,MAAO,EACPC,KAAM,EACNC,KAAM,EACNC,IAAK,EACLC,MAAO,EACPC,MAAO,EACPC,IAAK,IAKDC,QAEJ,IAEE,QAA2B,IAAhB,CAAAC,IAAA,oBAAAC,SAAAC,QAAA,OAAAC,cAAAC,YAAAC,KAAAC,GAAA,WAAAA,EAAAC,QAAAC,eAAAF,EAAAG,KAAA,IAAAC,IAAA,eAAAT,SAAAU,SAAAN,QAA+B,oBAAAJ,SAAAC,QAAA,OAAAC,cAAAC,YAAAC,KAAAC,GAAA,WAAAA,EAAAC,QAAAC,eAAAF,EAAAG,KAAA,IAAAC,IAAA,eAAAT,SAAAU,SAAAN,KAAA,IAAeO,EACvD,OAAO,CAEX,CAAA,MAEA,CAGA,GAA0B,oBAAfC,iBAA4D,IAAvBA,WAAWC,QACzD,IACE,QAASD,WAAWC,OACtB,CAAA,MAEA,CAKF,QADsC,oBAAZC,UAA2BA,SAAkC,iBAAhBA,QAAQC,KAC7B,iBAAzBD,QAAQC,IAAIC,WACH,eAAzBF,QAAQC,IAAIC,QAIvB,KAEMC,EAA8B,oBAAXC,QAA8C,oBAAblB,SACpDmB,EAA+B,oBAAfP,WAA6BA,WAAgC,oBAAXM,OAAyBA,OAASE,OAGpGC,EAAmBF,EAAOG,SAAW,CAAA,EACrCC,EAAY,CAAA,EAClB,IAAIC,GAAY,EACZC,EAAgB,KAOpB,SAASC,EAAWC,GAClB,GAAqB,iBAAVA,EAAoB,CAE7B,MAAMC,EAAMzC,EAAOG,OACbuC,EAAM1C,EAAOS,MACnB,OAAOkC,KAAKF,IAAIE,KAAKD,IAAIF,EAAOC,GAAMC,EACxC,CACA,GAAqB,iBAAVF,EAAoB,CAC7B,MAAMI,EAAMJ,EAAMK,cAClB,GAAI5C,OAAO6C,UAAUC,eAAeC,KAAKhD,EAAQ4C,GAC/C,OAAO5C,EAAO4C,EAElB,CACA,OAAO,IACT,CAuDA,SAASK,EAAYC,EAAYC,GAC/B,MAAMC,EAAWhB,EAAUc,IAAehB,EAAiBgB,UAAuB,GAClF,OAAO,YAAiCG,GAEtC,GAAIf,GAAiBa,EACnB,OAAOC,EAASE,MAAMpB,EAAkBmB,EAI5C,CACF,CA4EA,MAAME,EAAkB,CAEtB,OAAAC,CAAQC,EAAU,IAChB,GAAIpB,EAKF,OAHIoB,QAAoC,IAAlBA,EAAQjB,OAC5BkB,KAAKC,SAASF,EAAQjB,MAAO,CAAEoB,UAAWH,EAAQG,UAE7CF,KAGT,IAAK1B,IAAWE,EAGd,OADAG,GAAY,EACLqB,KAGTpB,EA3EJ,SAA+BuB,GAE7B,MAAMC,EAAWvB,EAAWsB,GAC5B,GAAiB,OAAbC,EAAmB,OAAOA,EAG9B,MAAMC,EAtFR,WACE,IAAKjC,GAAiC,oBAAbkC,WAA6BA,SAASC,OAAQ,OAAO,KAC9E,IACE,MAAMC,EAAS,IAAIC,gBAAgBH,SAASC,QACtCG,EAAO,CAAC,WAAY,WAAY,WACtC,IAAA,MAAWC,KAAKD,EAAM,CACpB,MAAME,EAAIJ,EAAOK,IAAIF,GACrB,GAAS,MAALC,EAAW,CACb,MAAME,EAASjC,EAAW+B,GAC1B,GAAe,OAAXE,EAAiB,OAAOA,CAC9B,CACF,CACF,CAAA,MAEA,CACA,OAAO,IACT,CAsEmBC,GACjB,GAAiB,OAAbV,EAAmB,OAAOA,EAG9B,MAAMW,EAvER,WACE,IAAK5C,KAAe,iBAAkBE,GAAS,OAAO,KACtD,IACE,MAAMsC,EAAItC,EAAO2C,aAAaC,QAAQ,kBACtC,GAAS,MAALN,EAAW,CACb,MAAME,EAASjC,EAAW+B,GAC1B,GAAe,OAAXE,EAAiB,OAAOA,CAC9B,CACF,CAAA,MAEA,CACA,OAAO,IACT,CA2DsBK,GACpB,OAAoB,OAAhBH,EAA6BA,EAG1BnC,EAAW5B,EAnHM,QACC,OAmH3B,CA4DoBmE,CAAsBrB,EAAQjB,OAE9C,MAAMuC,EA3DV,WACE,MAAMA,EAAU,IAAK7C,GAGf8C,EAAe,CAEnB5E,MAAOJ,EAAOI,MACdC,KAAML,EAAOK,KAGbC,KAAMN,EAAOM,KACbC,IAAKP,EAAOM,KACZ2E,IAAKjF,EAAOM,KACZ4E,MAAOlF,EAAOM,KAGdE,MAAOR,EAAOQ,MACd2E,MAAOnF,EAAOQ,MACd4E,eAAgBpF,EAAOQ,MACvB6E,SAAUrF,EAAOQ,MACjB8E,KAAMtF,EAAOQ,MACb+E,QAASvF,EAAOQ,MAChBgF,QAASxF,EAAOQ,MAChBC,MAAOT,EAAOS,OAIhB,IAAA,MAAWgF,KAAQxF,OAAOmE,KAAKY,GAC7B5C,EAAUqD,GAAQvD,EAAiBuD,UAAiB,GACpDV,EAAQU,GAAQxC,EAAYwC,EAAMT,EAAaS,IAQjD,OAJArD,EAAUsD,OAASxD,EAAiBwD,QAAA,MAAkB,GACtDX,EAAQW,OAnEV,WACE,MAAMtC,EAAWhB,EAAUsD,QAAUxD,EAAiBwD,cAAkB,GACxE,OAAO,SAAuBC,KAActC,GAE1C,IAAKsC,EACH,OAAIrD,GAAiBtC,EAAOI,MACnBgD,EAASE,MAAMpB,EAAkB,CAACyD,KAActC,SAEzD,CAIJ,CACF,CAsDmBuC,GAGVb,CACT,CAqBoBc,GAShB,OAPA7D,EAAOG,QAAU4C,EAEjB1C,GAAY,EAGZL,EAAO8D,oBAAsBpC,KAEtBA,IACT,EAGA,SAAAqC,GACE,IAAK1D,EAAW,OAAOqB,KAEvB,IACE1B,EAAOG,QAAUD,CACnB,CAAA,MAEA,CAEA,OADAG,GAAY,EACLqB,IACT,EAIA,QAAAC,CAASnB,GAAOoB,QAAEA,GAAU,GAAU,CAAA,GACpC,MAAMY,EAASjC,EAAWC,GAC1B,OAAe,OAAXgC,IACJlC,EAAgBkC,EACZZ,GA3JR,SAAuBoC,GACrB,GAAKlE,GAAe,iBAAkBE,EACtC,IACE,MAAMY,EAAmC,iBAAtBoD,EACfA,EACsB,OAAtBA,EACE,KACA/F,OAAOgG,QAAQjG,GAAQkG,KAAK,GAAIC,KAASA,IAAQH,KAAqB,IAAM,KAC9EpD,EACFZ,EAAO2C,aAAayB,QAAQ,iBAAkBxD,GAE9CZ,EAAO2C,aAAa0B,WAAW,iBAEnC,CAAA,MAEA,CACF,CA4IMC,CAAc9D,IAHYkB,IAM9B,EAGA6C,SAAA,IACSjE,EAIT,YAAAkE,GACE,MAAMC,EAAQxG,OAAOgG,QAAQjG,GAAQkG,KAAK,GAAIC,KAASA,IAAQ7D,GAC/D,OAAOmE,EAAQA,EAAM,GAAK,IAC5B,EAGA,YAAAC,EAAa9C,QAAEA,GAAU,GAAU,CAAA,GACjC,OAAOF,KAAKC,SAAS,OAAQ,CAAEC,WACjC,EAEA,UAAA+C,EAAW/C,QAAEA,GAAU,GAAU,CAAA,GAC/B,OAAOF,KAAKC,SAAS,QAAS,CAAEC,WAClC,EAEA,MAAAzD,EAAOyD,QAAEA,GAAU,GAAU,CAAA,GAC3B,OAAOF,KAAKC,SAAS,SAAU,CAAEC,WACnC,EAEA,OAAAgD,EAAQhD,QAAEA,GAAU,GAAU,CAAA,GAC5B,OAAOF,KAAKC,SAAShD,EAAQ,QAAU,OAAQ,CAAEiD,WACnD,EAEA,QAAAiD,EAASjD,QAAEA,GAAU,GAAU,CAAA,GAC7B,OAAOF,KAAKC,SAAS,QAAS,CAAEC,WAClC,EAGA,kBAAAkD,CAAmBtE,EAAOuE,GACxB,MAAMC,EAAO1E,EACPkC,EAASjC,EAAWC,GAC1B,GAAe,OAAXgC,GAAiC,mBAAPuC,SAA0BA,MACxDzE,EAAgBkC,EAChB,IACE,OAAOuC,GACT,CAAA,QACEzE,EAAgB0E,CAClB,CACF,EAGAhH,UClVF,MAAMiH,wBAAwBC,EAAAA,iBAC1B,WAAAC,CAAY1D,EAAU,IAClB2D,MAAM,IACC3D,EACH4D,UAAW,qBAAqB5D,EAAQ4D,WAAa,KAAKC,SAI9D5D,KAAK6D,cAAgC,IAArB9D,EAAQ8D,UAAyB9D,EAAQ8D,SACzD7D,KAAK8D,YAAc/D,EAAQ+D,aAAe,SAC1C9D,KAAK+D,UAAYhE,EAAQgE,WAAa,OACtC/D,KAAKgE,SAAW,GAChBhE,KAAKiE,eAAiB,GAGtBjE,KAAKkE,eAAkC,IAAtBnE,EAAQmE,WAA0BnE,EAAQmE,SAC/D,CAKA,kBAAAC,CAAmBC,GACf,IAAKA,GAA0B,IAAjBA,EAAMC,OAChB,MAAO,GAIX,MAAMC,qBAAgBC,IAGtBH,EAAMI,QAAQC,IACLH,EAAUI,IAAID,EAAKE,KACpBL,EAAUM,IAAIH,EAAKE,GAAI,IAChBF,EACHI,SAAU,GACV/F,MAAO,EACPgG,aAAa,IAKrB,MAAMC,EAAYN,EAAKzE,KAAK8D,aACxBiB,GAAaA,EAAUJ,KAAOL,EAAUI,IAAIK,EAAUJ,KACtDL,EAAUM,IAAIG,EAAUJ,GAAI,IACrBI,EACHF,SAAU,GACV/F,MAAO,EACPgG,aAAa,MAKzB,MAAME,EAAY,GAGlBV,EAAUE,QAAQ,CAACS,EAAUC,KACzB,MAAMC,EAAef,EAAM5B,QAAU4C,EAAET,KAAOO,IAAWD,EACnDI,EAAWF,EAAanF,KAAK8D,cAAca,GAEjD,GAAIU,GAAYf,EAAUI,IAAIW,GAAW,CACrC,MAAMC,EAAShB,EAAUzD,IAAIwE,GAC7BC,EAAOT,SAASU,KAAKN,GACrBK,EAAOR,aAAc,CACzB,MACIE,EAAUO,KAAKN,KAKvB,MAAMO,EAAkB,CAACC,EAAO3G,EAAQ,KACpC2G,EAAMjB,QAAQkB,IACVA,EAAK5G,MAAQA,EACT4G,EAAKb,SAASR,OAAS,IACvBqB,EAAKb,SAASc,KAAK,CAACC,EAAGC,KAAOD,EAAE7D,MAAQ,IAAI+D,cAAcD,EAAE9D,MAAQ,KACpEyD,EAAgBE,EAAKb,SAAU/F,EAAQ,OAQnD,OAHAkG,EAAUW,KAAK,CAACC,EAAGC,KAAOD,EAAE7D,MAAQ,IAAI+D,cAAcD,EAAE9D,MAAQ,KAChEyD,EAAgBR,GAETA,CACX,CAMA,WAAAe,CAAYN,EAAOO,EAAS,GAAIC,EAAoB,IAmBhD,OAlBAR,EAAMjB,QAAQ,CAACkB,EAAMQ,KACjBR,EAAKS,aAAeD,IAAUT,EAAMpB,OAAS,EAC7CqB,EAAKU,mBAAqB,IAAIH,GAI9B,MAAMI,EAAmBJ,EAAkBK,MAAMC,GAAQA,GAMzD,GALAb,EAAKc,kBAAoBH,GAAoBX,EAAKS,gBAC5CT,EAAKb,UAAqC,IAAzBa,EAAKb,SAASR,QAErC2B,EAAOT,KAAKG,GAERA,EAAKb,UAAYa,EAAKb,SAASR,OAAS,EAAG,CAC3C,MAAMoC,EAAW,IAAIR,EAAmBP,EAAKS,cAC7CnG,KAAK+F,YAAYL,EAAKb,SAAUmB,EAAQS,EAC5C,IAGGT,CACX,CAQA,oBAAAU,CAAqBC,GACjB,IAAA,IAASvB,EAAI,EAAGA,EAAIuB,EAAStC,OAAQe,IAAK,CACtC,MAAMX,EAAOkC,EAASvB,GACtBX,EAAKmC,kBAAoB,GAEzB,IAAA,IAASC,EAAI,EAAGA,EAAIpC,EAAK3F,MAAO+H,IAAK,CAEjC,IAAIC,GAAe,EACnB,IAAA,IAASC,EAAI3B,EAAI,EAAG2B,EAAIJ,EAAStC,OAAQ0C,IAAK,CAC1C,MAAMC,EAAaL,EAASI,GAC5B,GAAIC,EAAWlI,QAAU+H,EAAI,EAAG,CAE5BC,GAAe,EACf,KACJ,CAAA,GAAWE,EAAWlI,OAAS+H,EAE3B,KAGR,CACApC,EAAKmC,kBAAkBC,GAAKC,CAChC,CACJ,CACJ,CAKA,mBAAAG,GACI,IAAKjH,KAAKkH,WAIN,OAHAlH,KAAKmH,cAAgB,GACrBnH,KAAKgE,SAAW,QAChBhE,KAAKiE,eAAiB,IAK1B,MAAMG,EAAQpE,KAAKkH,WAAWE,SAC9BpH,KAAKgE,SAAWhE,KAAKmE,mBAAmBC,GACxCpE,KAAKiE,eAAiBjE,KAAK+F,YAAY/F,KAAKgE,UAG5ChE,KAAK0G,qBAAqB1G,KAAKiE,gBAG/BjE,KAAKmH,cAAgBnH,KAAKiE,eAE1BjE,KAAKqH,mBACT,CAKA,sBAAAC,GACI,MAAO,mQAQX,CAKA,mBAAAC,CAAoB9C,GAEhB,IAAI+C,EAAUxH,KAAKyH,aACnBD,EAAUA,EAAQE,QAAQ,iBAAkB,CAACC,EAAOC,IACnC,aAATA,EACO5H,KAAK6D,SAAW,OAAS,GAE7B7D,KAAK6H,eAAepD,EAAMmD,IAAS,IAK1CJ,EADAxH,KAAK6D,SACK2D,EAAQE,QAAQ,6CAA8C,MAE9DF,EAAQE,QAAQ,2CAA4C,IAK1E,IAAII,EAAe,GACnB,GAAI9H,KAAKkE,WAAaO,EAAK3F,MAAQ,EAC/B,IAAA,IAASsG,EAAI,EAAGA,EAAIX,EAAK3F,MAAOsG,IACLA,IAAMX,EAAK3F,MAAQ,EAMtCgJ,GAAgB,gBADCrD,EAAK0B,aAAe,yBAA2B,mCAM5D2B,GAFqBrD,EAAKmC,mBAAqBnC,EAAKmC,kBAAkBxB,GAEtD,+CAEA,iCAWhC,MAAO,8CAJaX,EAAKK,YAAc,gBAAkB,KACrCL,EAAK0B,aAAe,iBAAmB,wBAIuB1B,EAAK3F,0EAEzEgJ,4GAGAN,yDAIlB,CAKA,YAAAO,GACI,OAAO/H,KAAKgE,QAChB,CAKA,eAAAgE,CAAgBC,GACZ,MAAMC,EAAW,CAACzC,EAAO0C,KACrB,IAAA,MAAWzC,KAAQD,EAAO,CACtB,GAAIC,EAAKf,KAAOwD,EACZ,OAAOzC,EAAKb,SAEhB,GAAIa,EAAKb,SAASR,OAAS,EAAG,CAC1B,MAAM+D,EAAQF,EAASxC,EAAKb,SAAUsD,GACtC,GAAIC,EAAO,OAAOA,CACtB,CACJ,CACA,OAAO,MAGX,OAAOF,EAASlI,KAAKgE,SAAUiE,IAAW,EAC9C,ECxQJ,MAAMI,gBAAgBC,EAAAA,KAClB,WAAA7E,CAAY1D,EAAU,IAClB2D,MAAM,CACFjG,QAAS,MACTkG,UAAW,UACXgB,GAAI,aACD5E,IAGPC,KAAKuI,yBAAYhE,IACjBvE,KAAKwI,eAAiB,KACtBxI,KAAKyI,aAAe,KACpBzI,KAAK0I,WAAa3I,EAAQ2I,WAC1B1I,KAAK2I,aAAc,EACnB3I,KAAK4I,aAAe7I,EAAQ8I,OAAS,gBACrC7I,KAAK8I,WAAa,KACd9I,KAAKD,QAAQgJ,cAAa/I,KAAK+I,YAAc/I,KAAKD,QAAQgJ,aAK9D/I,KAAKgJ,kBAAoBjJ,EAAQiJ,mBAAqB,SACtDhJ,KAAKiJ,oBAAsB,KAEvBjJ,KAAK4I,cACL5I,KAAKkJ,SAASlJ,KAAK4I,cAIvB5I,KAAKmJ,gBAAgBpJ,GAGrBC,KAAKoJ,uBAG8B,IAA/BrJ,EAAQsJ,oBACRrJ,KAAKsJ,yBAEb,CAEAP,YAAc,88BAwBd,YAAMQ,SACI7F,MAAM6F,SAGZ,MAAMC,EAAMxJ,KAAKyJ,SACXC,EAASF,GAAKE,OAEpB,GAAIA,EAAQ,CACR,MAAMC,EAAcD,EAAOE,iBACvBD,GACA3J,KAAK6J,yBAAyBF,EAEtC,CAGA3J,KAAK8J,qBAEL9J,KAAK+J,WAAa,IAAIxG,gBAAgB,CAClCyG,UAAU,EACVC,gBAAgB,EAChBC,WAAY,eACZC,YAAa,2BACbC,WAAYC,EAAAA,UACZ5C,aAAc,4NAOlBzH,KAAKsK,SAAStK,KAAK+J,YACnB/J,KAAK+J,WAAWQ,GAAG,gBAAkBC,IAEjCxK,KAAKyJ,SAASgB,eAAeD,EAAIE,SAErC1K,KAAK+J,WAAWQ,GAAG,OAAS9F,IAExBzE,KAAK2K,mBAEb,CAEA,eAAAC,GACmC,WAA3B5K,KAAKgJ,kBACLhJ,KAAK6K,yBAGL7K,KAAK8K,SAAS,WACd9K,KAAK+K,YAAa,EAClB/K,KAAKgL,SAEb,CAEA,eAAAL,GACmC,WAA3B3K,KAAKgJ,kBACDhJ,KAAKiJ,qBACLjJ,KAAKiJ,oBAAoBgC,QAI7BjL,KAAK8K,SAAS,WACd9K,KAAK+K,YAAa,EAClB/K,KAAKgL,SAEb,CAEA,uBAAAE,GACIlL,KAAK4K,iBACT,CAEA,+BAAMO,GAEF,MAAM1J,EAAQzB,KAAKyJ,SAAS2B,YAE5B,SADqBC,UAAOC,QAAQ,6CAA6C7J,EAAMZ,IAAI,oBAC/E,CACRb,KAAKyJ,SAAS8B,cACd,IAAIjG,EAAS,IAAIkG,EAAAA,MAAM,CAAC7G,GAAIlD,EAAMZ,IAAI,qBAChCyE,EAAOmG,QACbzL,KAAKyJ,SAASgB,eAAenF,GAC7BtF,KAAKyJ,SAASiC,aAClB,CACJ,CAKA,2BAAMb,GAEF,MAAM3D,EAAa,IAAImD,YAGjBN,EAAa,IAAIxG,gBAAgB,CACnC6G,WAAYC,EAAAA,UACZnD,aACAyE,aAAc,CAAC,QACfzB,WAAY,KACZ0B,kBAAmB,mBACnBC,WAAY,KACZC,UAAW7M,KAAKF,IAAI,IAAKV,OAAO0N,YAAc,KAC9C9B,gBAAgB,EAChBpG,UAAU,EACVC,YAAa,SACbC,UAAW,OACXiI,gBAAgB,EAChBC,eAAe,EACfC,WAAY,GACZhI,WAAW,IAIflE,KAAKiJ,oBAAsB,IAAIoC,UAAO,CAClCc,KAAMpC,EACNqC,KAAM,KACNC,OAAQ,KACRC,eAAe,EACfC,YAAY,EACZC,QAAS,GACTC,aAAa,IAIjB1C,EAAWQ,GAAG,gBAAkBC,IAE5BxK,KAAKyJ,SAASgB,eAAeD,EAAIE,OAC7B1K,KAAKiJ,qBACLjJ,KAAKiJ,oBAAoBgC,SAKjCjL,KAAKiJ,oBAAoBsB,GAAG,SAAU,KAClCvK,KAAKiJ,oBAAoByD,UACzB1M,KAAKiJ,oBAAsB,aAIzBjJ,KAAKiJ,oBAAoB+B,QAAO,EAAM7N,SAASgP,MACrDnM,KAAKiJ,oBAAoB0D,MAC7B,CAKA,wBAAA9C,CAAyB+C,GAErB,IAAA,MAAYC,EAAUC,KAAe9M,KAAKuI,MACtC,KAAIuE,EAAWC,WAAc/M,KAAKyJ,SAAS2B,cAEvCpL,KAAKgN,kBAAkBF,EAAYF,GAsBnC,OApBA5M,KAAKiN,eAAeJ,GACpB7M,KAAKyI,aAAemE,EAGpB5M,KAAKkN,uBACLlN,KAAKmN,qBAAqBP,GAG1B5M,KAAKgL,SAKLhL,KAAKoN,KAAK,qBAAsB,CAC5BP,WACAD,QACAS,OAAQP,EACRQ,QAAStN,QAGN,EAIf,OAAO,CACX,CAKA,oBAAAkN,GACI,IAAA,MAAYL,EAAUC,KAAe9M,KAAKuI,MACtC,IAAA,MAAW9D,KAAQqI,EAAW1I,OAAS,GAEnC,GADAK,EAAK8I,QAAS,EACV9I,EAAKI,SACL,IAAA,MAAW2I,KAAS/I,EAAKI,SACrB2I,EAAMD,QAAS,CAKnC,CAKA,oBAAAJ,CAAqBP,GACjB,MAAMa,EAAkBC,IACpB,IAAKA,EAAG,MAAO,IAEf,MAAMC,EAAUC,mBAAmBF,GACnC,OAAOC,EAAQE,WAAW,KAAOF,EAAU,IAAIA,KAG7CG,EAAcL,EAAeb,GAGnC,IAAA,MAAYC,EAAUC,KAAe9M,KAAKuI,MACtC,IAAIuE,EAAWC,WAAc/M,KAAKyJ,SAAS2B,YAE3C,IAAA,MAAW3G,KAAQqI,EAAW1I,OAAS,GAAI,CAEvC,GAAIK,EAAKmI,MAAO,CACZ,MAAMmB,EAAYN,EAAehJ,EAAKmI,OACtC,GAAI5M,KAAKgO,YAAYF,EAAaC,GAG9B,OAFAtJ,EAAK8I,QAAS,EACdvN,KAAKiO,eAAiBxJ,GACf,CAEf,CAGA,GAAIA,EAAKI,SACL,IAAA,MAAW2I,KAAS/I,EAAKI,SACrB,GAAI2I,EAAMZ,MAAO,CACb,MAAMsB,EAAaT,EAAeD,EAAMZ,OACxC,GAAI5M,KAAKgO,YAAYF,EAAaI,GAG9B,OAFAV,EAAMD,QAAS,EACf9I,EAAK8I,QAAS,GACP,CAEf,CAGZ,CAGJ,OAAO,CACX,CAKA,iBAAAP,CAAkBF,EAAYF,GAC1B,MAAMa,EAAkBC,IACpB,IAAKA,EAAG,MAAO,IAEf,MAAMC,EAAUC,mBAAmBF,GACnC,OAAOC,EAAQE,WAAW,KAAOF,EAAU,IAAIA,KAG7CG,EAAcL,EAAeb,GAGnC,IAAA,MAAWnI,KAAQqI,EAAW1I,OAAS,GAAI,CAEvC,GAAIK,EAAKmI,MAAO,CACZ,MAAMmB,EAAYN,EAAehJ,EAAKmI,OACtC,GAAI5M,KAAKgO,YAAYF,EAAaC,GAC9B,OAAO,CAEf,CAGA,GAAItJ,EAAKI,SACL,IAAA,MAAW2I,KAAS/I,EAAKI,SACrB,GAAI2I,EAAMZ,MAAO,CACb,MAAMsB,EAAaT,EAAeD,EAAMZ,OACxC,GAAI5M,KAAKgO,YAAYF,EAAaI,GAC9B,OAAO,CAEf,CAGZ,CAEA,OAAO,CACX,CAKA,WAAAF,CAAYvF,EAAcsF,GACtB,OAAO/N,KAAKyJ,SAASC,OAAOyE,cAAc1F,EAAcsF,EAC5D,CAEA,WAAAK,GACI,OAAIpO,KAAK8I,WACE,2EAEP9I,KAAK+K,WAAmB/K,KAAKqO,oBAC1BrO,KAAKsO,iBAChB,CAEA,iBAAAD,GACI,MAAO,2GAIX,CAEA,eAAAC,GACI,MAAO,u/CAyCX,CAKA,WAAAC,GACI,MAAO,CACH,WAAY,orGAiEZ,cAAe,0IAKf,aAAc,oEAGd,YAAa,4JAMrB,CAEA,cAAAC,GACI,OAAOxO,KAAK+I,WAChB,CAKA,OAAA0F,CAAQ1M,EAAMsL,GAqBV,OApBIA,EAAON,YAAcM,EAAOhB,SAC5BgB,EAAOhB,OAASrM,KAAKwO,kBAGzBxO,KAAKuI,MAAM3D,IAAI7C,EAAM,CACjBA,OACAgL,UAAWM,EAAON,WAAa,KAC/BV,OAAQgB,EAAOhB,QAAU,KACzBqC,OAAQrB,EAAOqB,QAAU,KACzBtK,MAAOiJ,EAAOjJ,OAAS,GACvBuK,KAAMtB,EAAOsB,MAAQ,CAAA,EACrBhL,UAAW0J,EAAO1J,WAAa,yBAK9B3D,KAAKwI,gBACNxI,KAAKiN,eAAelL,GAGjB/B,IACX,CAEA,cAAAiN,CAAelL,GACX/B,KAAK+K,YAAa,EAClB/K,KAAKwI,eAAiBzG,EACtB,MAAMsL,EAASrN,KAAK4O,uBAChBvB,EAAO1J,UACP3D,KAAK8K,SAASuC,EAAO1J,WAErB3D,KAAK8K,SAAS,UAEtB,CAKA,mBAAM+D,CAAc9M,GAChB,IAAK/B,KAAKuI,MAAM7D,IAAI3C,GAEhB,OADAtD,QAAQ9B,KAAK,SAASoF,gBACf/B,KAGX,MAAM8M,EAAa9M,KAAKuI,MAAM1H,IAAIkB,GAElC,IAAI+K,EAAWC,YACX/M,KAAK8O,cAAgBhC,EAEhB9M,KAAKyJ,SAAS2B,aAevB,OATApL,KAAKiN,eAAelL,SACd/B,KAAKgL,SAEXhL,KAAKoN,KAAK,eAAgB,CACtBP,SAAU9K,EACVsL,OAAQP,EACRQ,QAAStN,OAGNA,KAdCA,KAAK4K,iBAejB,CAEA,YAAAmE,CAAatN,GACT,IAAKA,EAED,OADAhD,QAAQ9B,KAAK,qBACN,KAGX,IAAIqS,EAAahP,KAAK8O,cAClBG,EAAe,KACnB,GAAIxN,EAAMyN,EAAEC,KACR,IAAA,MAAYtC,EAAUC,KAAe9M,KAAKuI,MAAO,CAI7C,GAFgBvI,KAAKoP,kBAAkBtC,EAAWC,UAAWtL,EAAMyN,EAAEC,MAExD,CACTH,EAAalC,EACb,KACJ,CAAoC,QAAzBA,EAAWC,YAClBkC,EAAenC,EAEvB,CAGJ,OAAKkC,GACMC,CAGf,CASA,iBAAAG,CAAkBrC,EAAWoC,GACzB,SAAKpC,IAAcoC,KAGfE,MAAMC,QAAQvC,GACPA,EAAUwC,SAASJ,GAIvBpC,IAAcoC,EACzB,CAEA,gBAAAK,CAAiB/N,GACb,IAAKA,EAED,YADAhD,QAAQ9B,KAAK,qBAIjB,IAAIqS,EAAahP,KAAK+O,aAAatN,GAEnC,GAAKuN,EAYL,OARAhP,KAAKiN,eAAe+B,EAAWjN,MAC/B/B,KAAKgL,SAELhL,KAAKoN,KAAK,eAAgB,CACtBP,SAAUmC,EAAWjN,KACrBsL,OAAQ2B,EACR1B,QAAStN,OAENA,KAXHvB,QAAQ9B,KAAK,iCAAiC8E,EAAM0N,OAY5D,CAKA,aAAAM,CAAc1N,GACV,OAAO/B,KAAKuI,MAAM1H,IAAIkB,IAAS,IACnC,CAKA,oBAAA6M,GACI,OAAO5O,KAAKwI,eAAiBxI,KAAKuI,MAAM1H,IAAIb,KAAKwI,gBAAkB,IACvE,CAKA,UAAAkH,CAAW3N,EAAM4N,GACb,MAAMC,EAAO5P,KAAKuI,MAAM1H,IAAIkB,GAC5B,OAAK6N,GAMLrT,OAAOsT,OAAOD,EAAMD,GAGhB3P,KAAKwI,iBAAmBzG,GACxB/B,KAAKgL,SAGFhL,OAZHvB,QAAQ9B,KAAK,SAASoF,gBACf/B,KAYf,CAKA,UAAA8P,CAAW/N,GAIP,GAHA/B,KAAKuI,MAAMwH,OAAOhO,GAGd/B,KAAKwI,iBAAmBzG,EAAM,CAC9B,MAAMiO,EAAiBX,MAAMY,KAAKjQ,KAAKuI,MAAM7H,QAC7CV,KAAKwI,eAAiBwH,EAAe3L,OAAS,EAAI2L,EAAe,GAAK,KACtEhQ,KAAKgL,QACT,CAEA,OAAOhL,IACX,CAKA,oBAAMkQ,GACF,MAAMC,EAAcnQ,KAAK4O,uBAEzB,IAAKuB,EACD,MAAO,CAAEA,YAAa,MAG1B,IAAIC,EAAU,CACVC,QAASrQ,KAAKyJ,SAAS4G,SAAW,KAClC5O,MAAOzB,KAAKyJ,SAAS2B,aAAe,KACpCkF,KAAMtQ,KAAKyJ,OAAO8G,YAAc,MAGpCvQ,KAAK2O,KAAO,CACRwB,YAAa,CACT9D,OAAQrM,KAAKwQ,qBAAqBL,EAAY9D,QAAU,GAAI+D,GAC5D1B,OAAQ1O,KAAKwQ,qBAAqBL,EAAYzB,QAAU,GAAI0B,GAC5DhM,MAAOpE,KAAKyQ,gBAAgBN,EAAY/L,MAAO+L,EAAYpD,WAC3D4B,KAAMwB,EAAYxB,KAClBjG,WAAY1I,KAAK0I,YAI7B,CAEA,mBAAMgI,GAEE1Q,KAAK2Q,mBAELC,WAAW,IAAM5Q,KAAK8J,qBAAsB,IAE5C9J,KAAK6Q,iBAEb,CAEA,aAAAC,CAAcC,GAUV,OATI/Q,KAAK8I,YACL9I,KAAKgR,YAAYhR,KAAK8I,WAAWnE,IAErC3E,KAAK8I,WAAaiI,EACdA,IACAA,EAAK5G,YAAc,gCACnBnK,KAAKsK,SAASyG,IAElB/Q,KAAKgL,SACEhL,IACX,CAEA,eAAAiR,GAMI,OALIjR,KAAK8I,aACL9I,KAAKgR,YAAYhR,KAAK8I,WAAWnE,IACjC3E,KAAK8I,WAAa,MAEtB9I,KAAKgL,SACEhL,IACX,CAKC,eAAAyQ,CAAgBrM,EAAO2I,GACnB,MAAMvD,EAAMxJ,KAAKyJ,SACX8G,EAAa/G,GAAK+G,WAClBnF,EAAc5B,GAAK4B,YAGnB8F,EAAwBtE,IAC1B,IAAIuE,EAAkBvE,EAStB,GANIA,EAAMiB,WAAW,OAASjB,EAAM2C,SAAS,OAEzC4B,EAAkB,SADDvE,EAAMwE,UAAU,IAAM,UAKvCrE,GAAa3B,GAAeA,EAAYzG,GAAI,CAC5C,MAAM0M,EAAYF,EAAgB5B,SAAS,KAAO,IAAM,IACxD,MAAO,GAAG4B,IAAkBE,UAAkBjG,EAAYzG,IAC9D,CACA,OAAOwM,GAGX,OAAO/M,EAAMkN,IAAI,CAAC7M,EAAMyB,KAEpB,GAAa,KAATzB,GAAgC,iBAATA,GAAqBA,EAAK8M,QACjD,MAAO,CACHC,WAAW,EACX7M,GAAI,WAAWuB,KAKvB,GAAoB,iBAATzB,GAAqBA,EAAKgN,OACjC,MAAO,CACHC,UAAU,EACV/M,GAAI,UAAUuB,KAItB,MAAMyL,EAAgB,IAAKlN,GAG3B,GAAIkN,EAAcC,eACTrB,IAAeA,EAAWsB,cAAcF,EAAcC,cACvD,OAAO,KAKf,GAAID,EAAcG,kBAAmB,CACjC,MAAM/E,EAAY3B,GAAa8D,EAAEC,MAAQ/D,GAAa+D,KACtD,IAAKpC,IAAc/M,KAAKoP,kBAAkBuC,EAAcG,kBAAmB/E,GACvE,OAAO,IAEf,CAEA,GAA2B,UAAvB4E,EAAcxC,KAKf,OAJAwC,EAAcI,SAAU,EACnBJ,EAAchN,KACfgN,EAAchN,GAAK,aAAauB,KAE7ByL,EASV,GALKA,EAAchN,KACfgN,EAAchN,GAAK,OAAOuB,KAI1ByL,EAAc/E,MACd+E,EAAcpU,KAAO2T,EAAqBS,EAAc/E,YAC5D,GAAW+E,EAAcK,KAAM,CAE3B,MAAMC,EAAYN,EAAcK,KAAKnE,WAAW,KAAO8D,EAAcK,KAAO,IAAIL,EAAcK,OAC9FL,EAAcpU,KAAO2T,EAAqBe,GAC1CN,EAAc/E,MAAQ+E,EAAcpU,IACxC,CA4CA,OAvCIoU,EAAc9M,UACd8M,EAAc9M,SAAW8M,EAAc9M,SAASyM,IAAI9D,IAChD,MAAM0E,EAAiB,IAAK1E,GAG5B,GAAI0E,EAAeN,aAAerB,IACzBA,EAAWsB,cAAcK,EAAeN,aACzC,OAAO,KAKf,GAAIM,EAAeJ,kBAAmB,CAClC,MAAM/E,EAAY3B,GAAa8D,EAAEC,MAAQ/D,GAAa+D,KACtD,IAAKpC,IAAc/M,KAAKoP,kBAAkB8C,EAAeJ,kBAAmB/E,GACxE,OAAO,IAEf,CAGA,GAAImF,EAAetF,MACfsF,EAAe3U,KAAO2T,EAAqBgB,EAAetF,YAC9D,GAAWsF,EAAeF,KAAM,CAC5B,MAAMC,EAAYC,EAAeF,KAAKnE,WAAW,KAAOqE,EAAeF,KAAO,IAAIE,EAAeF,OACjGE,EAAe3U,KAAO2T,EAAqBe,GAC3CC,EAAetF,MAAQsF,EAAe3U,IAC1C,CAGA,OAAO2U,IACRC,OAAO3E,GAAmB,OAAVA,GAGnBmE,EAAc7M,eAAiB6M,EAAc9M,UAAY8M,EAAc9M,SAASR,OAAS,IAGzFsN,EAAc7M,aAAc,EAGzB6M,IACRQ,OAAO1N,GAAiB,OAATA,EACtB,CAQD,YAAA2N,CAAa3N,GACT,IAAKA,EAAKmI,QAAU5M,KAAKyI,aACrB,OAAO,EAGX,MAAMgF,EAAkBb,IACpB,IAAKA,EAAO,MAAO,IAEnB,MAAMe,EAAUC,mBAAmBhB,GACnC,OAAOe,EAAQE,WAAW,KAAOF,EAAU,IAAIA,KAG7CI,EAAYN,EAAehJ,EAAKmI,OAChCnE,EAAegF,EAAezN,KAAKyI,cAEzC,MAAkB,MAAdsF,GAAsC,MAAjBtF,GAIP,MAAdsF,GAAsC,MAAjBtF,IACdA,EAAaoF,WAAWE,IAActF,IAAiBsF,EAItE,CAKA,sBAAMsE,CAAiBzF,GAQnB,OAPA5M,KAAKyI,aAAemE,EAGpB5M,KAAKkN,uBACLlN,KAAKmN,qBAAqBP,SAEpB5M,KAAKgL,SACJhL,IACX,CAKA,+BAAMsS,CAA0BC,EAAOC,GACnC,MAAMC,EAAQD,EAAQE,cAAc,cAChCD,GACAA,EAAME,UAAUC,OAAO,UAE/B,CAKA,+BAAMC,CAA0BN,EAAOC,GACnCxS,KAAK8S,eACT,CAEA,qBAAAC,CAAsBC,EAAQT,EAAOU,GAGjC,OAFAjT,KAAK6O,cAAc,kBAEZ,CACX,CAEA,qBAAMqE,CAAgBF,EAAQT,EAAOU,GACjC,MAAM5F,EAASrN,KAAK4O,uBACpB,IAAKvB,EAAQ,OAGb,MAAM8F,EAAyB/O,IAC3B,IAAA,MAAWK,KAAQL,EAAO,CACtB,GAAKK,EAAKuO,QAAUA,GAAWvO,EAAK2O,QAEhC,OADA3O,EAAK2O,QAAQJ,EAAQT,EAAOU,EAAIjT,KAAKyJ,WAC9B,EAGX,GAAIhF,EAAKI,UAAYJ,EAAKI,SAASR,OAAS,GACpC8O,EAAsB1O,EAAKI,UAC3B,OAAO,CAGnB,CACA,OAAO,GAGX,OAAOsO,EAAsB9F,EAAOjJ,MACxC,CAKA,YAAAiP,GACI,OAAOhE,MAAMY,KAAKjQ,KAAKuI,MAAM7H,OACjC,CAKA,OAAA4S,CAAQvR,GACJ,OAAO/B,KAAKuI,MAAM7D,IAAI3C,EAC1B,CAKA,UAAAwR,GAII,OAHAvT,KAAKuI,MAAMiL,QACXxT,KAAKwI,eAAiB,KACtBxI,KAAKgL,SACEhL,IACX,CAKA,WAAAyT,CAAY9E,GACR,MAAMwB,EAAcnQ,KAAK4O,uBAKzB,OAJIuB,IACAA,EAAYxB,KAAO,IAAKwB,EAAYxB,QAASA,GAC7C3O,KAAKgL,UAEFhL,IACX,CAKA,WAAA0T,GACI,MAAMvD,EAAcnQ,KAAK4O,uBACzB,OAAOuB,EAAcA,EAAYxB,KAAO,CAAA,CAC5C,CAKA,mBAAAvF,GACI,MAAMI,EAAMxJ,KAAKyJ,SACbD,GAAOA,EAAImK,SACXnK,EAAImK,OAAOpJ,GAAG,CAAC,gBAAkBoE,IAC7B3O,KAAK4T,eAAejF,KAExBnF,EAAImK,OAAOpJ,GAAG,gBAAkBoE,IAC5B3O,KAAKwP,iBAAiBb,EAAKlN,SAE/B+H,EAAImK,OAAOpJ,GAAG,sBAAwBoE,IAClC3O,KAAKgL,WAGjB,CAoBA,cAAA4I,CAAejF,GACX,GAAIA,EAAKqD,MAAQrD,EAAKqD,KAAKpF,MAAO,CAC9B,MAAMA,EAAQ+B,EAAKqD,KAAKpF,MACxB,GAAI5M,KAAKiO,gBAAkBjO,KAAKgO,YAAYpB,EAAO5M,KAAKiO,eAAerB,OACnE,OAMJ,GAFqB5M,KAAK6J,yBAAyB+C,GAI/C,OAKJ,MAAMiH,EAAgBlF,EAAKqD,KAAK8B,aAAenF,EAAKqD,KAAKjS,SAAS+T,aAAe,KACjF,GAAID,GAAiB7T,KAAKuI,MAAM7D,IAAImP,GAKhC,OAJA7T,KAAKiN,eAAe4G,GACpB7T,KAAKkN,4BACLlN,KAAKgL,SAOT,IAAI+I,EAAe,KACnB,IAAA,MAAYlH,EAAUC,KAAe9M,KAAKuI,MACtC,IAAKuE,EAAWC,UAAW,CACvBgH,EAAelH,EACf,KACJ,CAGAkH,GAAgB/T,KAAKwI,iBAAmBuL,GACxC/T,KAAKiN,eAAe8G,GAKxB/T,KAAKkN,uBACLlN,KAAKqS,iBAAiBzF,GACtB5M,KAAKgL,QACT,CACJ,CAKI,aAAA8H,GACI,MAAMkB,EAAkB7W,SAASuV,cAAc,qBAC/C,IAAKsB,EAAiB,OAGtBhU,KAAKiU,kBAEL,MAAMC,EAAuBF,EAAgBrB,UAAUwB,SAAS,oBAqBhE,OApB0BH,EAAgBrB,UAAUwB,SAAS,iBAIzDH,EAAgBrB,UAAUyB,OAAO,gBACjCpU,KAAK2I,aAAc,EACnB3I,KAAK6Q,mBACEqD,GAEPF,EAAgBrB,UAAUyB,OAAO,oBACjCpU,KAAK2I,aAAc,EACnB3I,KAAK6Q,oBAGLmD,EAAgBrB,UAAU0B,IAAI,oBAC9BrU,KAAK2I,aAAc,EAEnBiI,WAAW,IAAM5Q,KAAK8J,qBAAsB,MAGzC9J,IACX,CAKA,eAAAsU,CAAgBC,GACZ,MAAMP,EAAkB7W,SAASuV,cAAc,qBAC/C,IAAKsB,EAAiB,OAAOhU,KAK7B,OAFAgU,EAAgBrB,UAAUyB,OAAO,mBAAoB,gBAE7CG,GACJ,IAAK,YACDP,EAAgBrB,UAAU0B,IAAI,oBAC9BrU,KAAK2I,aAAc,EACnB,MACJ,IAAK,SACDqL,EAAgBrB,UAAU0B,IAAI,gBAC9BrU,KAAK2I,aAAc,EACnB,MAEJ,QACI3I,KAAK2I,aAAc,EAe3B,OAVI3I,KAAK2I,aAEL3I,KAAKiU,kBAELrD,WAAW,IAAM5Q,KAAK8J,qBAAsB,MAG5C9J,KAAK6Q,kBAGF7Q,IACX,CAKA,kBAAA8J,GAKI,OAHA9J,KAAK6Q,kBAGA7Q,KAAK2Q,oBAKO3Q,KAAKwS,QAAQgC,iBAAiB,0BAEtChQ,QAASiQ,IACd,MAAMC,EAAUD,EAAK/B,cAAc,aAEnC,GAAIgC,GAAWA,EAAQC,YAAY/Q,OAAQ,CACvC,MAAMgR,EAAcF,EAAQC,YAAY/Q,OASxC,GANA6Q,EAAKI,aAAa,iBAAkB,WACpCJ,EAAKI,aAAa,oBAAqB,SACvCJ,EAAKI,aAAa,gBAAiBD,GACnCH,EAAKI,aAAa,oBAAqB,QAGnCxW,OAAOyW,WAAazW,OAAOyW,UAAUC,QAAS,CAE9C,MAAMlM,EAAQ4L,EAAKO,aAAa,sBAC1B5I,EAAOqI,EAAKO,aAAa,qBAG/B,IAAIC,EAAc,GACdpM,IAAOoM,GAAe,WAAWpM,MACjCuD,IAAM6I,GAAe,WAAW7I,KAGpC,MAAM8I,EAAiB,CACnBC,UAAW,QACXC,UAAW,OACXC,QAAS,QACTC,MAAO,CAAE3I,KAAM,IAAK1B,KAAM,KAC1BsK,mBAAoB,CAAC,MAAO,SAAU,SAIpCC,EAAeP,EAAYrR,OAC7B4R,IACAN,EAAeD,YAAcO,GAGjC,MAAMC,EAAU,IAAIpX,OAAOyW,UAAUC,QAAQN,EAAMS,GAGnDT,EAAKiB,iBAAmBD,EAGxBhB,EAAKkB,iBAAiB,QAAS,KAC3BF,EAAQxK,SAGZwJ,EAAKkB,iBAAiB,OAAQ,KAC1BF,EAAQxK,QAEhB,CACJ,IAIJjL,KAAK4V,0BAEE5V,MAhEIA,IAiEf,CAEA,eAAA6Q,GAyBI,OAvBA7Q,KAAK6V,6BAEY7V,KAAKwS,QAAQgC,iBAAiB,oDAEtChQ,QAASiQ,IAEd,MAAMqB,EAAkBrB,EAAKiB,kBAAoBrX,OAAOyW,WAAWC,SAASgB,YAAYtB,GACpFqB,IAEAA,EAAgB7K,OAChB6K,EAAgBE,kBAIbvB,EAAKiB,iBAGZjB,EAAKwB,gBAAgB,kBACrBxB,EAAKwB,gBAAgB,qBACrBxB,EAAKwB,gBAAgB,iBACrBxB,EAAKwB,gBAAgB,uBAGlBjW,IACX,CAKA,eAAAkW,GACI,MAAMlC,EAAkB7W,SAASuV,cAAc,qBAC/C,OAAKsB,EAEDA,EAAgBrB,UAAUwB,SAAS,gBAC5B,SACAH,EAAgBrB,UAAUwB,SAAS,oBACnC,YAEA,SAPkB,QASjC,CAKA,gBAAAxD,GACI,MAAkC,cAA3B3Q,KAAKkW,iBAChB,CAKA,gBAAAC,CAAiBC,GAGb,OAFApW,KAAK0I,WAAa0N,EAClBpW,KAAKgL,SACEhL,IACX,CAKJ,eAAAmJ,CAAgBpJ,GACZ,GAAIA,EAAQwI,MACR,IAAA,MAAWqH,KAAQ7P,EAAQwI,MACvBvI,KAAKyO,QAAQmB,EAAK7N,KAAM6N,QAErB7P,EAAQ6P,OACf7P,EAAQ6P,KAAK7N,KAAOhC,EAAQ6P,KAAK7N,MAAQ,UACzC/B,KAAKyO,QAAQ1O,EAAQ6P,KAAK7N,KAAMhC,EAAQ6P,MAEhD,CAKA,uBAAAgG,GAEI5V,KAAKqW,sBAAwB,IAAMrW,KAAKiU,kBACxCjU,KAAKwS,QAAQmD,iBAAiB,SAAU3V,KAAKqW,sBAAuB,CAAEC,SAAS,IAG/EtW,KAAKuW,qBAAuB,IAAMvW,KAAKiU,kBAC3BjU,KAAKyJ,SAIjBzJ,KAAKwW,oBAAsB,IAAMxW,KAAKiU,kBACtC5V,OAAOsX,iBAAiB,OAAQ3V,KAAKwW,qBAGrCxW,KAAKyW,sBAAyBC,IACZ,WAAVA,EAAExX,KACFc,KAAKiU,mBAGb9W,SAASwY,iBAAiB,UAAW3V,KAAKyW,sBAC9C,CAKA,0BAAAZ,GACQ7V,KAAKqW,wBACLrW,KAAKwS,QAAQmE,oBAAoB,SAAU3W,KAAKqW,8BACzCrW,KAAKqW,uBAGZrW,KAAKwW,sBACLnY,OAAOsY,oBAAoB,OAAQ3W,KAAKwW,4BACjCxW,KAAKwW,qBAGZxW,KAAKyW,wBACLtZ,SAASwZ,oBAAoB,UAAW3W,KAAKyW,8BACtCzW,KAAKyW,sBAEpB,CAKA,eAAAxC,GACqBjU,KAAKwS,QAAQgC,iBAAiB,oDACtChQ,QAASiQ,IACd,MAAMgB,EAAUhB,EAAKiB,kBAAoBrX,OAAOyW,WAAWC,SAASgB,YAAYtB,GAC5EgB,GACAA,EAAQxK,SAKQ9N,SAASqX,iBAAiB,iBAClChQ,QAAQiR,IACpBA,EAAQrB,UAEhB,CAKA,qBAAMwC,GAEF5W,KAAK6Q,wBAGCnN,MAAMkT,iBAChB,CAKA,uBAAAtN,GACI,MAAMuN,EAAc,KAChB,MAAMC,EAAWzY,OAAO0Y,YAAc,IAChC/C,EAAkB7W,SAASuV,cAAc,qBAE3CsB,IACI8C,EACA9C,EAAgBrB,UAAU0B,IAAI,kBAE9BL,EAAgBrB,UAAUyB,OAAO,iBAAkB,kBAM/DyC,IACAxY,OAAOsX,iBAAiB,SAAUkB,EACtC,CAKA,oBAAOG,CAAcjX,EAAU,IAC3B,OAAO,IAAIsI,QAAQ,CACfQ,MAAO,gBACPH,YAAY,EACZW,oBAAoB,KACjBtJ,GAEX,CAKA,oBAAOkX,CAAclX,EAAU,IAC3B,OAAO,IAAIsI,QAAQ,CACfQ,MAAO,gBACPH,YAAY,EACZW,oBAAoB,KACjBtJ,GAEX,CAKA,eAAAmX,CAAgBrO,GAQZ,OANA7I,KAAKmX,YAAY,4CAGjBnX,KAAK4I,aAAeC,EACpB7I,KAAKkJ,SAASL,GAEP7I,IACX,CAKA,IAAA2M,GACI,OAAO3M,KAAKsU,gBAAgB,SAChC,CAEA,IAAArJ,GACI,OAAOjL,KAAKsU,gBAAgB,SAChC,CAEA,QAAA8C,GACI,OAAOpX,KAAKsU,gBAAgB,YAChC,CAEA,MAAA+C,GACI,OAAOrX,KAAKsU,gBAAgB,SAChC,CAKA,WAAAgD,GACI,MAAMC,EAAevX,KAAKwS,QAAQE,cAAc,mBAChD,GAAI6E,EAAc,CACdA,EAAa5E,UAAU0B,IAAI,SAG3B,MAAMmD,EAAc,KAChBD,EAAa5E,UAAUyB,OAAO,SAC9BmD,EAAaZ,oBAAoB,QAASa,IAG9CD,EAAa5B,iBAAiB,QAAS6B,EAAa,CAAEC,MAAM,IAC5D7G,WAAW4G,EAAa,IAC5B,CACA,OAAOxX,IACX,CAKA,iBAAA0X,CAAkB7K,EAAU8K,EAAM/K,EAAOgL,EAAO,aAC5C,MAAMhI,EAAO5P,KAAKuI,MAAM1H,IAAIgM,GAa5B,OAZI+C,IACAA,EAAKxL,MAAQwL,EAAKxL,OAAS,GAC3BwL,EAAKxL,MAAMmB,KAAK,CACZoS,OACA/K,QACAgL,SAGA5X,KAAKwI,iBAAmBqE,GACxB7M,KAAKgL,UAGNhL,IACX,CAKA,aAAA6X,CAAc9V,EAAMsK,EAAQjI,GACxB,MAAMwL,EAAO,CACT7N,OACAsK,SACAjI,SAMJ,OAHApE,KAAKyO,QAAQ1M,EAAM6N,GACnB5P,KAAK6O,cAAc9M,GAEZ/B,IACX,EC//CJ,MAAM8X,mBAAmBxP,EAAAA,KACrB,WAAA7E,CAAY1D,EAAU,IAClB2D,MAAM,CACFjG,QAAS,MACTkG,UAAW,iBACR5D,IAIPC,KAAK+X,MAAQhY,EAAQgY,OAAS,UAC9B/X,KAAKoM,KAAOrM,EAAQqM,MAAQ,KAC5BpM,KAAKgY,UAAgC,IAArBjY,EAAQiY,SACxBhY,KAAKiY,iBAA8C,IAA5BlY,EAAQkY,gBAC/BjY,KAAKkY,gBAAkBnY,EAAQmY,kBAAmB,EAGlDlY,KAAKmY,YAAc,IACvB,CAEA,iBAAM/J,GACF,MAAmB,YAAfpO,KAAK+X,MACE/X,KAAKoY,qBACU,eAAfpY,KAAK+X,MACL/X,KAAKqY,wBAETrY,KAAKsY,oBAChB,CAEA,kBAAAA,GACI,MAAO,irDAuCX,CAEA,kBAAAF,GACI,MAAO,ybAYX,CAEA,qBAAAC,GACI,MAAO,o/DA8CX,CAEA,oBAAMnI,SACIxM,MAAMwM,iBAEZ,MAAM8B,EAAOhS,KAAKmY,YACZI,IAAYvG,EAGdA,IAEWA,EAAKwG,MACCxG,EAAKyG,YACZzG,EAAKjQ,KACDiQ,EAAK0G,SACT1G,EAAK4F,KACD5F,EAAK2G,SACE3G,EAAK4G,gBACT5G,EAAK6G,aAK1B,MAAMC,EAAgB9G,GAAMjS,SAAS+Y,eAChB9G,GAAM8G,eACN9G,GAAMvO,aAAarE,WAAW0Z,eAC9B,GAErB9Y,KAAK2O,KAAO,CACR4J,UACAQ,UAAW/G,GAAMwG,OAASxG,GAAMyG,aAAezG,GAAMjQ,MAAQiQ,GAAM0G,UAAY,GAC/EC,SAAU3G,GAAM4F,MAAQ5F,GAAM2G,UAAY,GAC1CC,gBAAiB5G,GAAM4G,iBAAmB5G,GAAM6G,aAAe,GAC/Db,SAAUhY,KAAKgY,SACfC,gBAAiBjY,KAAKiY,gBACtBC,gBAAiBlY,KAAKkY,gBACtBc,YAAahH,GAAMjS,SAASiZ,aAAehH,GAAMgH,aAAe,GAChEC,QAASH,EACTI,WAAYJ,EAAczU,OAAS,EACnC+H,KAAMpM,KAAKoM,MAGiBpM,KAAK2O,IACzC,CAKA,aAAMwK,CAAQnH,GAEVhS,KAAKmY,YAAcnG,EAGfA,SAEMhS,KAAKgL,QAGnB,CAKA,OAAAoO,GACI,OAAOpZ,KAAKmY,WAChB,CAKA,qBAAMjF,CAAgBF,EAAQT,EAAOC,GAEjC,OAAIxS,KAAKmY,aAA0D,mBAApCnY,KAAKmY,YAAYkB,sBACtCrZ,KAAKmY,YAAYkB,eAAerG,EAAQT,EAAOC,IAC9C,IAIXxS,KAAKoN,KAAK,SAAU,CAChB4F,SACAT,QACAC,UACAR,KAAMhS,KAAKmY,eAGR,EACX,EC1NJ,MAAMmB,mBAAmBC,EAAAA,KACvB,WAAA9V,CAAY1D,EAAU,IACpB2D,MAAM,CACJgV,SAAU,gBACV9L,MAAO,UACP4L,MAAO,gBACPG,SAAU,iBACVa,SAAU,uuFAoEPzZ,IAILC,KAAKyZ,WAAa,KAClBzZ,KAAK0Z,kBAAoB,IAC3B,CAKA,cAAMC,CAASnZ,EAAS,GAAIoZ,EAAQ,CAAA,SAC5BlW,MAAMiW,SAASnZ,EAAQoZ,GAGzBpZ,EAAOwR,MACThS,KAAKyZ,WAAajZ,EAAOwR,KACzBhS,KAAK0Z,kBAAoBlZ,EAAOwR,KAAKjS,SAAWS,EAAOwR,KAAK6H,aAAe,CAAA,GAClED,EAAM5H,OAEfhS,KAAK8Z,eAAiBF,EAAM5H,KAEhC,CAKA,aAAA+H,CAAcC,GAGZ,OAFAha,KAAKyZ,WAAaO,EAClBha,KAAK0Z,kBAAoBM,GAAcja,SAAWia,GAAcH,aAAe,CAAA,EACxE7Z,IACT,CAKA,iBAAMia,GACJ,MAAMzQ,EAAMxJ,KAAKyJ,SAGXyQ,EAAc1Q,GAAK+G,YAAc/G,GAAK2Q,oBAAsB,KAGlE,IAAIC,EAAiB,KACrB,GAAIpa,KAAKyZ,WAAY,CACnB,MAAM7H,EAAc5R,KAAK0Z,mBAAmB9H,aACzB5R,KAAKyZ,WAAW1Z,SAAS6R,aACzB5R,KAAKyZ,WAAWI,aAAajI,YAEhDwI,EAAiB,CACf3B,YAAazY,KAAKyZ,WAAWhB,aAAezY,KAAKyZ,WAAWf,UAAY1Y,KAAKyZ,WAAWjB,OAAS,eACjGE,SAAU1Y,KAAKyZ,WAAWf,SAC1B9L,MAAO5M,KAAKyZ,WAAW7M,MACvBiM,YAAa7Y,KAAKyZ,WAAWb,iBAAmB5Y,KAAKyZ,WAAWZ,YAChEF,SAAU3Y,KAAKyZ,WAAWd,UAAY,kBACtC0B,oBAAqBzI,EAAc,CACjCA,YAAavC,MAAMC,QAAQsC,GAAeA,EAAc,CAACA,IACvD,KAER,MAAW5R,KAAK8Z,iBACdM,EAAiB,CACf3B,YAAazY,KAAK8Z,eAClBpB,SAAU1Y,KAAK8Z,eACfnB,SAAU,oBAId,MAAO,CACLc,WAAYW,EACZF,YAAaA,EAAc,CACzBI,SAAUJ,EAAYI,UAAYJ,EAAYnY,MAAQmY,EAAYK,OAAS,eAC3ExY,KAAMmY,EAAYnY,KAClBwY,MAAOL,EAAYK,OACjB,KACJC,WAAYN,EAEhB,CAKA,wBAAMO,CAAmBlI,EAAOC,GAC9BD,EAAMmI,iBAGFrc,OAAOsc,QAAQtW,OAAS,EAC1BhG,OAAOsc,QAAQC,aAGT5a,KAAK6a,mBAAmBtI,EAAOC,EAEzC,CAKA,wBAAMqI,CAAmBtI,EAAOC,GAC9BD,EAAMmI,iBAEN,MAAMlR,EAAMxJ,KAAKyJ,SACbD,QACIA,EAAIsR,oBAGVzc,OAAOiC,SAAS/C,KAAO,GAE3B,CAKA,uBAAMwd,CAAkBxI,EAAOC,GAC7BD,EAAMmI,iBAEN,MAAMlR,EAAMxJ,KAAKyJ,SAGjB,GAAID,EACF,UACQA,EAAIwR,SAAS,QACrB,OAASte,GAEP,UACQ8M,EAAIyR,SAAS,SACrB,OAASC,GAEPlb,KAAKoN,KAAK,iBAAkB,CAC1B+N,UAAWnb,KAAKyZ,YAAY7M,OAASvO,OAAOiC,SAAS8a,WAIvDxK,WAAW,KACTpH,GAAK6R,WAAW,kDACf,IACL,CACF,CAEJ,CAKA,aAAMC,SACE5X,MAAM4X,UAGZ,MAAM5C,EAAW1Y,KAAKyZ,YAAYf,UAAY1Y,KAAK8Z,eAC/CpB,GACF1Y,KAAKub,QAAQ,CACX/C,MAAO,mBAAmBE,MAK9Bja,QAAQ9B,KAAK,yBAA0B,CACrCqV,KAAMhS,KAAKyZ,YAAYf,UAAY1Y,KAAK8Z,eACxClN,MAAO5M,KAAKyZ,YAAY7M,MACxBgF,YAAa5R,KAAK0Z,mBAAmB9H,YACrC4J,0BAAA,IAAeC,MAAOC,eAE1B,CAKA,kBAAOC,CAAYnS,EAAKwQ,GACtB,MAAMP,EAAa,IAAIH,WAEvB,OADAG,EAAWM,cAAcC,GAClBxQ,EAAIwR,SAASvB,EACtB,ECpPF,MAAMmC,qBAAqBrC,EAAAA,KACzB,WAAA9V,CAAY1D,EAAU,IACpB2D,MAAM,CACJgV,SAAU,MACV9L,MAAO,OACP4L,MAAO,uBACPG,SAAU,eACVa,SAAU,s0CAiCPzZ,IAILC,KAAK6b,KAAO,IACd,CAKA,cAAMlC,CAASnZ,EAAS,GAAIoZ,EAAQ,CAAA,SAC5BlW,MAAMiW,SAASnZ,EAAQoZ,GAGzBpZ,EAAOqb,OACT7b,KAAK6b,KAAOrb,EAAOqb,MAEjBjC,EAAMiC,OACR7b,KAAK6b,KAAOjC,EAAMiC,KAEtB,CAKA,OAAAC,CAAQD,GAEN,OADA7b,KAAK6b,KAAOA,GAAQ,KACb7b,IACT,CAKA,wBAAMya,CAAmBlI,EAAOwJ,GAC9BxJ,EAAMmI,iBAGFrc,OAAOsc,QAAQtW,OAAS,EAC1BhG,OAAOsc,QAAQC,aAGT5a,KAAK6a,mBAAmBtI,EAAOwJ,EAEzC,CAKA,wBAAMlB,CAAmBtI,EAAOwJ,GAC9BxJ,EAAMmI,iBAEN,MAAMlR,EAAMxJ,KAAKyJ,SACbD,QACIA,EAAIsR,oBAGVzc,OAAOiC,SAAS/C,KAAO,GAE3B,CAKA,aAAM+d,SACE5X,MAAM4X,UAGRtb,KAAK6b,MACP7b,KAAKub,QAAQ,CACX/C,MAAO,SAASxY,KAAK6b,mBAKzBpd,QAAQ9B,KAAK,iBAAkB,CAC7Bkf,KAAM7b,KAAK6b,KACXL,0BAAA,IAAeC,MAAOC,eAE1B,CAKA,kBAAOM,CAAYxS,EAAKqS,GACtB,MAAMI,EAAe,IAAIL,aAEzB,OADAK,EAAaH,QAAQD,GACdI,EAAajR,QACtB,ECnHa,MAAMkR,kBAAkBC,EAAAA,OACnC,WAAA1Y,CAAY4J,EAAS,IAEjB3J,MAAM2J,GAENrN,KAAKoc,cAAgB/O,EAAOC,QAc5BtN,KAAKqc,aAAehP,EAAOiP,QAAU,CAAA,EAGjCjP,EAAOkP,SAAWlP,EAAOiP,SACzBtc,KAAKqc,aAAehP,EAAOkP,QAI/Bvc,KAAKwc,eAAiBnP,EAAOmP,iBAAkB,EAC/Cxc,KAAKyc,iBAAmBpP,EAAOqP,YAAc,CAAA,EAG7C1c,KAAKsN,QAAU,KACftN,KAAKsc,OAAS,KACdtc,KAAKuc,OAAS,KACdvc,KAAK0c,WAAa,KAClB1c,KAAK2c,aAAe,IAAIC,eAGxB5c,KAAKoL,YAAc,KAEdpL,KAAK8W,WAIN9W,KAAK6c,iBAAmB7c,KAAKoc,cAAcU,mBAAoB,EAH/D9c,KAAK6c,iBAAmB7c,KAAK+c,qBACxB/c,KAAKoc,cAAcU,mBAAoB,GAIhD9c,KAAKgd,qBAELhd,KAAKid,MAAQ,IAAIC,eACjBld,KAAKqL,OAASA,EAAAA,QAEdrL,KAAKmd,aAAa,SAAU7D,YAC5BtZ,KAAKmd,aAAa,MAAOvB,aAC7B,CAKA,WAAMwB,SAGIpd,KAAKqd,kBAEXrd,KAAK2T,OAAOpJ,GAAG,oBAAqB,KAChCvK,KAAK2c,aAAaW,cAClBtd,KAAKud,KAAKC,YACVxd,KAAKyd,cAAc,QAIvBzd,KAAK2T,OAAOpJ,GAAG,cAAe,KAC1BvK,KAAK2c,aAAaW,cAClBtd,KAAKud,KAAKC,YACVxd,KAAKyd,cAAc,QAIvBzd,KAAK2T,OAAOpJ,GAAG,gBAAiB,KACvBvK,KAAKuQ,YACVvQ,KAAK2c,aAAae,sBAAsB1d,QAG5CA,KAAK2T,OAAOpJ,GAAG,gBAAiBvK,KAAK2d,eAAeC,KAAK5d,OAErDA,KAAKuQ,kBAECvQ,KAAK6d,yBAIT7d,KAAK8d,cAGX9d,KAAK+d,WAAY,EAGjB/d,KAAK2T,OAAOvG,KAAK,YAAa,CAAE5D,IAAKxJ,MAGzC,CAEA,qBAAMqd,GACF,MAAMW,EAAche,KAAK2c,aAAasB,mBAGtC,GAA2B,WAAvBD,EAAYhL,OAEZ,OADAhT,KAAK2T,OAAOvG,KAAK,oBAAqB,CAAE5D,IAAKxJ,QACtC,EAIX,GAA2B,YAAvBge,EAAYhL,gBACYhT,KAAK2c,aAAae,sBAAsB1d,OAG5D,OAAO,EAKf,MAAMke,EAAQle,KAAK2c,aAAawB,mBAGhC,GAAIne,KAAKuQ,WAEL,OADAvQ,KAAK2c,aAAayB,iBAAiBpe,OAC5B,EAIXA,KAAKud,KAAKc,aAAaH,EAAMA,OAC7B,MAAM5N,EAAO,IAAIgO,OAAK,CAAE3Z,GAAIuZ,EAAMK,cAC5BC,QAAalO,EAAK7E,QACxB,OAAK+S,EAAKC,SAMVze,KAAKyd,cAAcnN,GACnBtQ,KAAK2c,aAAayB,iBAAiBpe,OAC5B,IAPHA,KAAK2c,aAAaW,cAClBtd,KAAK2T,OAAOvG,KAAK,oBAAqB,CAAE5D,IAAKxJ,KAAMtD,MAAO8hB,EAAK9hB,SACxD,EAMf,CAKA,sBAAMmhB,GAEF,MACMa,EADY,IAAIje,gBAAgBpC,OAAOiC,SAASC,QACzBM,IAAI,SAG3B8d,EAAUD,GAAc1e,KAAK4e,oBAEnC,GAAID,EACA,IACI,MAAMld,EAAQ,IAAI+J,EAAAA,MAAM,CAAE7G,GAAIga,IACxBH,QAAa/c,EAAMgK,QACzB,IAAK+S,EAAKC,UAAYD,EAAK7P,KAAKkQ,OAG5B,OAFA7e,KAAK8e,wBACLrgB,QAAQ9B,KAAK,+BAAgC6hB,EAAKO,YAItD/e,KAAKoL,YAAc3J,EAEfid,GACA1e,KAAKgf,kBAAkBL,GAGvB3e,KAAKuQ,aACLvQ,KAAKuQ,WAAW0O,OAAS,IAAIC,eACvBlf,KAAKuQ,WAAW0O,OAAOE,cAAc1d,EAAMkD,KAIrD3E,KAAK2T,OAAOvG,KAAK,eAAgB,CAAE3L,MAAOzB,KAAKoL,aAGnD,OAAS1O,GAGL,GAFA+B,QAAQ9B,KAAK,+BAAgCD,GAEzCgiB,IAAe1e,KAAK4e,oBAEpB5e,KAAKof,6BACEV,EAAY,CAEnB,MAAMW,EAAgBrf,KAAK4e,oBAC3B,GAAIS,GAAiBA,IAAkBX,EACnC,IACI,MAAMY,EAAgB,IAAI9T,EAAAA,MAAM,CAAE7G,GAAI0a,UAChCC,EAAc7T,QACpBzL,KAAKoL,YAAckU,EACnBtf,KAAK2T,OAAOvG,KAAK,eAAgB,CAAE3L,MAAOzB,KAAKoL,aAEnD,OAASmU,GACL9gB,QAAQ9B,KAAK,wCAAyC4iB,GACtDvf,KAAKof,oBACT,CAER,CACJ,CAER,CAMA,oBAAM3U,CAAehJ,GACjB,MAAM+d,EAAgBxf,KAAKoL,YAC3BpL,KAAKoL,YAAc3J,EAGfA,GAASA,EAAMZ,IAAI,MACnBb,KAAKgf,kBAAkBvd,EAAMZ,IAAI,OAEjCb,KAAKof,qBAGLpf,KAAKuQ,aACLvQ,KAAKuQ,WAAW0O,OAAS,IAAIC,eACvBlf,KAAKuQ,WAAW0O,OAAOE,cAAc1d,EAAMkD,KAIrD3E,KAAK2T,OAAOvG,KAAK,gBAAiB,CAC9B3L,QACA+d,gBACAhW,IAAKxJ,OAGT,MAAMgS,EAAOhS,KAAKyf,iBAOlB,OANIzN,GACAA,EAAK0N,cAAcje,GAGvBzB,KAAK0J,OAAOiW,UAAU,CAACle,MAAMA,EAAMkD,IAAK,CAAE+C,SAAS,IAE5C1H,IACX,CAKA,cAAA4f,GACI,OAAO5f,KAAKoL,WAChB,CAKA,sBAAM0T,GACF,MAAMU,EAAgBxf,KAAKoL,YAQ3B,OAPApL,KAAKoL,YAAc,KACnBpL,KAAKof,qBAELpf,KAAK2T,OAAOvG,KAAK,gBAAiB,CAC9BoS,gBACAhW,IAAKxJ,OAEFA,IACX,CAKA,iBAAAgf,CAAkBL,GACd,IACI,MAAMzf,EAAMc,KAAK6f,2BACjB5e,aAAayB,QAAQxD,EAAKyf,EAAQmB,WACtC,OAASpjB,GACL+B,QAAQ9B,KAAK,kCAAmCD,EACpD,CACJ,CAKA,iBAAAkiB,GACI,IACI,MAAM1f,EAAMc,KAAK6f,2BACjB,OAAO5e,aAAaC,QAAQhC,EAChC,OAASxC,GAEL,OADA+B,QAAQ9B,KAAK,kCAAmCD,GACzC,IACX,CACJ,CAKA,kBAAA0iB,GACI,IACI,MAAMlgB,EAAMc,KAAK6f,2BACjB5e,aAAa0B,WAAWzD,EAC5B,OAASxC,GACL+B,QAAQ9B,KAAK,mCAAoCD,EACrD,CACJ,CAKA,wBAAAmjB,GACI,MAAO,iBACX,CAKA,gBAAAE,CAAiBC,GACb,IACI/e,aAAayB,QAAQ,iBAAkBsd,EAC3C,OAAStjB,GACL+B,QAAQ9B,KAAK,iCAAkCD,EACnD,CACJ,CAKA,mBAAAujB,GACI,OAAQjgB,KAAKoL,WACjB,CAKA,kBAAA4R,GACI,MAAM5H,EAAsC,iBAAnBpV,KAAKoV,UACxBjY,SAASuV,cAAc1S,KAAKoV,WAC5BpV,KAAKoV,UAEX,IAAKA,EACD,MAAM,IAAI8K,MAAM,+BAA+BlgB,KAAKoV,aAIxD,MAAM+K,EAAcngB,KAAKoc,eAAiB7f,OAAOmE,KAAKV,KAAKoc,eAAe/X,OAAS,EAC7E+b,EAAapgB,KAAKqc,cAAgB9f,OAAOmE,KAAKV,KAAKqc,cAAchY,OAAS,EAG1Egc,EAAgBrgB,KAAKwc,eAAiB,sQAOxC,iJAMJpH,EAAUkL,UAAY,2EAEZH,EAAc,kCAAoC,sEAE9CC,EAAa,iCAAmC,2BAChDC,0DAMdrgB,KAAKugB,cAAgB,kBAGrBnL,EAAUzC,UAAU0B,IAAI,oBAGxBrU,KAAKwgB,wBAGLxgB,KAAKygB,kBAAkBrL,EAC3B,CAKA,2BAAMoL,SACIxgB,KAAK0gB,qBACL1gB,KAAK2gB,oBACL3gB,KAAK4gB,kBACX5gB,KAAK6gB,mBACT,CAKA,kBAAMH,GACG1gB,KAAKoc,eAA4D,IAA3C7f,OAAOmE,KAAKV,KAAKoc,eAAe/X,SAE3DrE,KAAKsN,QAAU,IAAIjF,QAAQ,CACvB8B,YAAa,oBACVnK,KAAKoc,sBAGNpc,KAAKsN,QAAQtC,SACvB,CAKA,iBAAM2V,GACG3gB,KAAKqc,cAA0D,IAA1C9f,OAAOmE,KAAKV,KAAKqc,cAAchY,SAGzDrE,KAAKsc,OAAS,IAAIwE,SAAO,CACrB3W,YAAa,gBACb4W,UAAW/gB,KAAKqc,aAAa2E,OAAShhB,KAAKghB,OAAShhB,KAAKwY,MACzDyI,WAAYjhB,KAAKqc,aAAa4E,YAAc,IAC5CC,UAAWlhB,KAAKqc,aAAa6E,WAAalhB,KAAKkhB,UAC/CC,SAAUnhB,KAAKqc,aAAa+E,WAAa,GACzCC,WAAYrhB,KAAKqc,aAAagF,YAAc,GAC5CC,YAAathB,KAAKqc,aAAaiF,aAAe,OAC9CC,kBAAmBvhB,KAAKqc,aAAakF,oBAAqB,KACvDvhB,KAAKqc,qBAGNrc,KAAKsc,OAAOtR,SAGlBhL,KAAKuc,OAASvc,KAAKsc,OACvB,CAKA,qBAAMsE,GACF,IAAK5gB,KAAKwc,eAAgB,OAE1Bxc,KAAK0c,WAAa,IAAI5E,WAAW,CAC7B3N,YAAa,cACb4N,MAAO/X,KAAKyc,iBAAiB1E,OAAS,UACtCC,UAA6C,IAAnChY,KAAKyc,iBAAiBzE,SAChCC,iBAA2D,IAA1CjY,KAAKyc,iBAAiBxE,gBACvCC,gBAAiBlY,KAAKyc,iBAAiBvE,kBAAmB,KACvDlY,KAAKyc,mBAIZ,MAAM+E,EAAkBrkB,SAASskB,eAAe,eAC5CD,SACMxhB,KAAK0c,WAAW1R,QAAO,EAAMwW,EAE3C,CAKA,iBAAAX,GAUI,GARA1jB,SAASwY,iBAAiB,QAAUpD,IAC5BA,EAAMmP,OAAOC,QAAQ,oCACrBpP,EAAMmI,iBACN1a,KAAK8S,mBAKTzU,OAAOujB,eAAgB,CACvB,MAAMC,EAAiB,IAAID,eAAe,KACtC5hB,KAAK8hB,qBAETD,EAAeE,QAAQ5kB,SAASgP,MAChCnM,KAAKgiB,gBAAkBH,CAC3B,MAEI7hB,KAAKiiB,eAAiB,IAAMjiB,KAAK8hB,mBACjCzjB,OAAOsX,iBAAiB,SAAU3V,KAAKiiB,gBAI3CjiB,KAAK8hB,kBACT,CAKA,aAAAhP,GACI,IAAK9S,KAAKsN,QAAS,OAEnB,MAAM8H,EAAYjY,SAASuV,cAAc,qBACnCoE,EAAW9W,KAAK8W,WAElBA,EACA1B,EAAUzC,UAAUC,OAAO,iBAE3BwC,EAAUzC,UAAUC,OAAO,oBAC3B5S,KAAK6c,kBAAoB7c,KAAK6c,iBAG9B7c,KAAKkiB,iBAAiBliB,KAAK6c,mBAG/B7c,KAAK2T,OAAOvG,KAAK,kBAAmB,CAChC+U,UAAWniB,KAAK6c,iBAChBuF,OAAQtL,GAEhB,CAKA,gBAAAgL,GACI,MAAM1M,EAAYjY,SAASuV,cAAc,qBACzC,IAAK0C,EAAW,OAChB,MAAM0B,EAAW9W,KAAK8W,WAElBA,GACA1B,EAAUzC,UAAU0B,IAAI,iBACnBe,EAAUzC,UAAUwB,SAAS,iBAC9BiB,EAAUzC,UAAU0B,IAAI,iBAG5Be,EAAUzC,UAAUyB,OAAO,gBAAiB,gBAGhDpU,KAAK2T,OAAOvG,KAAK,qBAAsB,CAAEgV,OAAQtL,GACrD,CAEA,kBAAAuL,GACI,OAAOllB,SAASuV,cAAc,oBAClC,CAEA,QAAAoE,GACI,OAAOzY,OAAO0Y,WAAa,GAC/B,CAEA,eAAAuL,GACI,OAAOtiB,KAAKqiB,qBAAqB1P,UAAUwB,SAAS,gBACxD,CAKA,cAAM6G,CAAShJ,EAAM4H,EAAQ,CAAA,EAAIpZ,EAAS,CAAA,EAAIT,EAAU,IACpD,MAAMiG,QAAetC,MAAMsX,SAAShJ,EAAM4H,EAAOpZ,EAAQT,GAUzD,OARIC,KAAKsiB,mBACLtiB,KAAKqiB,qBAAqB1P,UAAU0B,IAAI,gBAGxCrU,KAAKmY,aACLnY,KAAKuiB,iBAAiBviB,KAAKmY,aAGxBnS,CACX,CAKA,gBAAAuc,CAAiBvQ,GAEThS,KAAKsN,SAAWtN,KAAKsN,QAAQkV,eAC7BxiB,KAAKsN,QAAQkV,cAAcxQ,EAAKpF,OAIhC5M,KAAKsc,QAAUtc,KAAKsc,OAAOkG,eAC3BxiB,KAAKsc,OAAOkG,cAAcxQ,EAAKpF,OAI/B5M,KAAK0c,YACL1c,KAAK0c,WAAWvD,QAAQnH,GAG5BhS,KAAK2T,OAAOvG,KAAK,sBAAuB,CAAE4E,QAC9C,CAKA,aAAAyL,CAAcnN,GACVtQ,KAAKuQ,WAAaD,EAEdtQ,KAAKsc,QACLtc,KAAKsc,OAAOmG,QAAQnS,GAIxBtQ,KAAK2T,OAAOvG,KAAK,sBAAuB,CAAEkD,QAC9C,CAKA,aAAAoS,GACI,OAAO1iB,KAAKuQ,UAChB,CAKA,gBAAA2R,CAAiBC,GACb,IACI,MAAMjjB,EAAMc,KAAK2iB,uBACjB1hB,aAAayB,QAAQxD,EAAK0jB,KAAKC,UAAUV,GAC7C,OAASzlB,GACL+B,QAAQ9B,KAAK,gCAAiCD,EAClD,CACJ,CAKA,gBAAAqgB,GACI,IACI,MAAM7d,EAAMc,KAAK2iB,uBACXG,EAAQ7hB,aAAaC,QAAQhC,GACnC,OAAiB,OAAV4jB,EAAiBF,KAAKG,MAAMD,GAAS,IAChD,OAASpmB,GAEL,OADA+B,QAAQ9B,KAAK,gCAAiCD,GACvC,IACX,CACJ,CAKA,oBAAAimB,GAGI,MAAO,GADQ3iB,KAAKwY,MAAQxY,KAAKwY,MAAM9Q,QAAQ,OAAQ,KAAKvI,cAAgB,gCAEhF,CAKA,iBAAAshB,CAAkBrL,EAAY,MACrBA,IACDA,EAAYjY,SAASuV,cAAc,sBAGlC0C,IAEDpV,KAAK6c,iBACLzH,EAAUzC,UAAU0B,IAAI,oBAExBe,EAAUzC,UAAUyB,OAAO,oBAEnC,CAKA,iBAAA4O,GACI,IACI,MAAM9jB,EAAMc,KAAK2iB,uBACjB1hB,aAAa0B,WAAWzD,EAC5B,OAASxC,GACL+B,QAAQ9B,KAAK,iCAAkCD,EACnD,CACJ,CAEA,oBAAMumB,GACF,MAAMtU,QAAa3O,KAAKkjB,SAAS,CAC7B1K,MAAO,kBACP2K,OAAQ,CACJ,CACIphB,KAAM,mBAAoBqhB,KAAM,WAChCC,MAAO,mBAAoBC,UAAU,EACrC5a,YAAY,EACZ6a,eAAe,EACfC,iBAAiB,GAGrB,CACIzhB,KAAM,eAAgBqhB,KAAM,WAAYC,MAAO,eAAgBC,UAAU,EACzE5a,YAAY,EACZ+a,cAAe,MAEfF,eAAe,EACfC,iBAAiB,EACjBE,WAAY,CAEVC,aAAc,iBAGpB,CACI5hB,KAAM,mBAAoBqhB,KAAM,WAAYC,MAAO,mBAAoBC,UAAU,EACjF5a,YAAY,EACZ+a,cAAe,MAEfF,eAAe,EACfC,iBAAiB,EACjBE,WAAY,CAGhC,IAGYE,YAAa,oBAEbjV,IACIA,EAAKkV,eAAiBlV,EAAKmV,iBAGP,aADD9jB,KAAKuQ,WAAWwT,KAAKpV,IAC/BkQ,OACL7e,KAAKid,MAAMwB,QAAQ,iCAEnBze,KAAKid,MAAMvgB,MAAM,6BAGrBsD,KAAKid,MAAMvgB,MAAM,0BAG7B,CAEA,cAAAihB,CAAe3K,GACX,OAAQA,EAAOA,QACX,IAAK,SACDhT,KAAK2c,aAAaW,cAClBtd,KAAKud,KAAKC,YACVxd,KAAKyd,cAAc,MACnB,MACJ,IAAK,UACDzd,KAAKgkB,cACL,MACJ,IAAK,kBACDhkB,KAAKijB,iBACL,MACJ,QACIxkB,QAAQ9B,KAAK,0BAA0BqW,KAEnD,CAEA,iBAAMgR,GACF,GAAKhkB,KAAKuQ,WAKV,IAII,MAAMvK,QAAeqF,EAAAA,QAAO4Y,cAAc,CACtCzL,MAAO,eACPpM,KAAM,KACN8X,aAAc,SACdxZ,MAAO1K,KAAKuQ,WACZ4S,OAAQ,CAEJ,CACIC,KAAM,SACNzL,KAAM,sBACN7Y,MAAO,EACPqlB,MAAO,qBAIX,CACIf,KAAM,QACNgB,QAAS,CAAEC,GAAI,GAAIC,GAAI,GACvB9L,MAAO,SACP2K,OAAQ,CACJ,CACIC,KAAM,QACNrhB,KAAM,SACNqK,KAAM,KACNmY,UAAW,CAAEC,MAAO,IAAKC,OAAQ,KACjCC,YAAa,qBACbC,KAAM,6BAMlB,CACIvB,KAAM,QACNgB,QAAS,CAAEC,GAAI,GAAIC,GAAI,GACvB9L,MAAO,UACP2K,OAAQ,CACJ,CACIC,KAAM,OACNrhB,KAAM,eACNshB,MAAO,eACPC,UAAU,EACVc,QAAS,GACTM,YAAa,oBAEjB,CACItB,KAAM,QACNrhB,KAAM,QACNshB,MAAO,gBACPC,UAAU,EACVc,QAAS,EACTM,YAAa,0BAEjB,CACItB,KAAM,MACNrhB,KAAM,eACNshB,MAAO,eACPe,QAAS,EACTM,YAAa,oBAMzB,CACItB,KAAM,QACNgB,QAAS,GACT5L,MAAO,mBACP2L,MAAO,OACPhB,OAAQ,CACJ,CACIC,KAAM,SACNrhB,KAAM,WACNshB,MAAO,WACPe,QAAS,EACTrkB,QAAS,CACL,CAAE6kB,MAAO,mBAAoBjN,KAAM,gBACnC,CAAEiN,MAAO,kBAAmBjN,KAAM,gBAClC,CAAEiN,MAAO,iBAAkBjN,KAAM,iBACjC,CAAEiN,MAAO,sBAAuBjN,KAAM,gBACtC,CAAEiN,MAAO,MAAOjN,KAAM,SAG9B,CACIyL,KAAM,SACNrhB,KAAM,WACNshB,MAAO,WACPe,QAAS,EACTrkB,QAAS,CACL,CAAE6kB,MAAO,KAAMjN,KAAM,WACrB,CAAEiN,MAAO,KAAMjN,KAAM,WACrB,CAAEiN,MAAO,KAAMjN,KAAM,UACrB,CAAEiN,MAAO,KAAMjN,KAAM,YAG7B,CACIyL,KAAM,SACNrhB,KAAM,sBACNshB,MAAO,sBACPe,QAAS,GAEb,CACIhB,KAAM,SACNrhB,KAAM,qBACNshB,MAAO,4BACPe,QAAS,GAEb,CACIhB,KAAM,SACNrhB,KAAM,iBACNshB,MAAO,iBACPe,QAAS,MAKzBS,WAAY,eACZC,WAAY,WAGZ9e,GAAUA,EAAOyY,QAOjBze,KAAK+kB,YAAY,iCACV/e,GAAWA,EAAOyY,OAKjC,OAAS/hB,GACL+B,QAAQ/B,MAAM,8BAA+BA,GAC7CsD,KAAKglB,UAAU,8BACnB,MAhJIhlB,KAAKglB,UAAU,iCAiJvB,CAKA,aAAMtY,GAGF1M,KAAKoL,YAAc,KAGfpL,KAAKgiB,iBACLhiB,KAAKgiB,gBAAgBiD,aAErBjlB,KAAKiiB,gBACL5jB,OAAOsY,oBAAoB,SAAU3W,KAAKiiB,gBAI1CjiB,KAAKsc,eACCtc,KAAKsc,OAAO5P,UAClB1M,KAAKsc,OAAS,KACdtc,KAAKuc,OAAS,MAGdvc,KAAKsN,gBACCtN,KAAKsN,QAAQZ,UACnB1M,KAAKsN,QAAU,YAIb5J,MAAMgJ,SAChB,CAKA,aAAOwY,CAAO7X,EAAS,IACnB,OAAO,IAAI6O,UAAU7O,EACzB,ECt6BW,MAAM8X,iBAAiB5L,EAAAA,KAClC,WAAA9V,CAAY1D,EAAU,IAClB2D,MAAM,CACF8U,MAAO,YACPK,YAAa,8BACbjB,KAAM,OACNuL,OAAQ,GACR3J,SAAU,mDACV7V,UAAW,4BACR5D,GAEX,CAEA,YAAMwJ,SACI7F,MAAM6F,eACNvJ,KAAKolB,kBACf,CAEA,aAAM9J,SACI5X,MAAM4X,UACRtb,KAAKqlB,gBAECrlB,KAAKolB,kBAEnB,CAEA,mBAAM1F,CAAcje,GACZzB,KAAKqlB,gBAECrlB,KAAKolB,kBAEnB,CAEA,cAAME,GACF,OAAItlB,KAAK0K,MACE1K,KAAK0K,MACL1K,KAAKyJ,SAAS2B,YACdpL,KAAKyJ,SAAS2B,YAElB,IACX,CAEA,sBAAMga,GAEEplB,KAAKqlB,iBACCrlB,KAAKqlB,SAAS3Y,UACpB1M,KAAKgR,YAAYhR,KAAKqlB,WAI1BrlB,KAAKqlB,SAAW,IAAIE,WAAS,CACzBpb,YAAa,sBACbgZ,OAAQnjB,KAAKD,QAAQojB,OACrBqC,oBAAoB,IAExBxlB,KAAKsK,SAAStK,KAAKqlB,UAEnB,MAAM3a,QAAc1K,KAAKslB,WACrB5a,GACA1K,KAAKqlB,SAASI,SAAS/a,EAE/B,ECyHC,MAACgb,EAAoB,IA5K1B,MACE,WAAAjiB,GACEzD,KAAK2lB,UAAYC,EAAAA,cACjB5lB,KAAK6lB,qCAAwBthB,GAC/B,CAWA,MAAAyG,CAAOwO,EAAU7K,EAAMmX,EAAW,CAAA,GAGhC,OAAOC,EAAAA,SAAS/a,OAAOwO,EAAU7K,EAAMmX,EACzC,CAOA,OAAAE,CAAQxM,GACN,MAAMyM,EAAWF,EAAAA,SAAShD,MAAMvJ,GAEhC,OADAxZ,KAAK6lB,kBAAkBjhB,IAAI4U,EAAUyM,GAC9BA,CACT,CASA,cAAAC,CAAeD,EAAUtX,EAAMmX,EAAW,CAAA,GACxC,OAAOC,EAAAA,SAAS/a,OAAOib,EAAUtX,EAAMmX,EACzC,CAKA,UAAAK,GACEnmB,KAAK6lB,kBAAkBrS,QACvBuS,EAAAA,SAASI,YACX,CAQA,KAAAC,CAAMlnB,EAAKsa,GAET,MAAO,CAAEta,MAAKsa,WAAUyM,SADPjmB,KAAKgmB,QAAQxM,GAEhC,CAOA,SAAA6M,CAAUnnB,GACR,IAAA,MAAYsa,EAAUyM,KAAajmB,KAAK6lB,kBACtC,GAAIrM,IAAata,GAAO+mB,IAAa/mB,EACnC,MAAO,CAAEA,MAAKsa,WAAUyM,YAG5B,OAAO,IACT,CAQA,iBAAAK,CAAkBvkB,EAAM4jB,GAEtB,OADA3lB,KAAK2lB,UAAUY,SAASxkB,EAAM4jB,GACvB3lB,IACT,CAOA,QAAAwmB,CAAShN,GACP,MAAO,gCAAgCiN,KAAKjN,EAC9C,CAUA,WAAAkN,CAAY/X,EAAMgY,GAChB,MAAMC,EAAY,IAAKjY,GAEvB,IAAA,MAAYzP,EAAK2nB,KAAetqB,OAAOgG,QAAQokB,GAE7C,GAAIhY,GAA4B,mBAAbA,EAAK9N,IACtB+lB,EAAU1nB,GAAOyP,EAAK9N,IAAI,GAAG3B,KAAO2nB,SAC/B,CAEL,MAAMjC,EAAQ5kB,KAAK8mB,iBAAiBnY,EAAMzP,GAC1C0nB,EAAU1nB,GAAOc,KAAK2lB,UAAUoB,KAAKnC,EAAOiC,EAC9C,CAGF,OAAOD,CACT,CAUA,gBAAAE,CAAiBE,EAAKnL,GACpB,IAAKmL,IAAQnL,EAAM,OAGnB,GAAImL,GAA0B,mBAAZA,EAAInmB,IACpB,OAAOmmB,EAAInmB,IAAIgb,GAIjB,MAAMnb,EAAOmb,EAAKoL,MAAM,KACxB,IAAIC,EAAUF,EAEd,IAAA,MAAW9nB,KAAOwB,EAAM,CACtB,GAAIwmB,QACF,OAKAA,GADGC,MAAMjoB,IAAQmQ,MAAMC,QAAQ4X,GACrBA,EAAQE,SAASloB,IAEjBgoB,EAAQhoB,EAEtB,CAEA,OAAOgoB,CACT,CASA,eAAAG,CAAgB7N,EAAU7K,GAGxB,MAAO,CAAE6K,WAAU7K,OACrB,GCxKF9O,EAAgBC,QAAQ,CAAEhB,MAAO,SAiFrB,MAACwoB,EAAiB,OACjBC,EAAe,WAE5BrhB,EAAe,CACbohB,iBACAC,m+JT4PqCxnB,GAAYF,EAAgBC,QAAQC"}
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../src/core/utils/ConsoleSilencer.js","../src/core/views/navigation/GroupSearchView.js","../src/core/views/navigation/Sidebar.js","../src/core/views/navigation/PageHeader.js","../src/core/pages/DeniedPage.js","../src/core/pages/NotFoundPage.js","../src/core/PortalApp.js","../src/core/forms/FormPage.js","../src/core/utils/MustacheFormatter.js","../src/core/views/user/ProfileOverviewSection.js","../src/core/views/user/ProfilePersonalSection.js","../src/core/models/Passkeys.js","../src/core/views/user/ProfileSecuritySection.js","../src/core/views/user/ProfileConnectedSection.js","../src/core/views/user/ProfileSessionsSection.js","../src/core/views/user/ProfileDevicesSection.js","../src/core/views/user/ProfileSecurityEventsSection.js","../src/core/views/user/ProfileNotificationsSection.js","../src/core/views/user/ProfileApiKeysSection.js","../src/core/views/user/ProfileGroupsSection.js","../src/core/views/user/ProfilePermissionsSection.js","../src/core/views/user/UserProfileView.js","../src/core/views/user/PasskeySetupView.js","../src/index.js"],"sourcesContent":["/**\n * ConsoleSilencer\n * \n * Purpose:\n * - Reduce console noise by filtering out non-critical logs in production.\n * - Keep critical logs (warn, error) by default in production.\n * - Allow runtime override via API, URL params, or localStorage without rebuilding.\n *\n * Usage:\n * import ConsoleSilencer from '@core/utils/ConsoleSilencer.js';\n * ConsoleSilencer.install(); // defaults to 'warn' in production, 'debug' in dev\n *\n * // Optional: set level at install-time\n * ConsoleSilencer.install({ level: 'error' });\n *\n * // Change level at runtime (optionally persist to localStorage)\n * ConsoleSilencer.setLevel('debug', { persist: true });\n *\n * // Temporarily change level around a block\n * ConsoleSilencer.withTemporaryLevel('debug', () => {\n * // noisy diagnostics here\n * });\n *\n * Runtime overrides (no code changes needed):\n * - URL: ?logLevel=debug|info|warn|error|silent\n * also accepts mojoLog and loglevel as param names\n * - localStorage: localStorage.setItem('MOJO_LOG_LEVEL', 'debug')\n *\n * Notes:\n * - This module is side-effect free until install() is called.\n * - Idempotent: calling install() multiple times won't double-wrap console.\n */\n\nconst LEVELS = Object.freeze({\n silent: 0,\n error: 1,\n warn: 2,\n info: 3,\n log: 3, // alias to info\n debug: 4,\n trace: 5,\n all: 5, // alias\n});\n\n// Resolve environment in a bundler/browser-safe way\n// __DEV__ can be injected by bundlers (e.g., Vite define)\nconst isDev = (() => {\n // Vite/ESM: import.meta.env.DEV if available\n try {\n /* eslint-disable-next-line no-undef */\n if (typeof import.meta !== 'undefined' && import.meta && import.meta.env && typeof import.meta.env.DEV !== 'undefined') {\n return !!import.meta.env.DEV;\n }\n } catch {\n // ignore\n }\n\n // Prefer global __DEV__ if present (e.g., defined via bundler define)\n if (typeof globalThis !== 'undefined' && typeof globalThis.__DEV__ !== 'undefined') {\n try {\n return !!globalThis.__DEV__;\n } catch {\n // fall through if __DEV__ is not directly evaluable\n }\n }\n\n // Fallback detection (best effort)\n const hasProcess = typeof process !== 'undefined' && process && typeof process.env === 'object';\n if (hasProcess && typeof process.env.NODE_ENV === 'string') {\n return process.env.NODE_ENV !== 'production';\n }\n // Default conservative behavior: assume production if unknown\n return false;\n})();\n\nconst isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\nconst GLOBAL = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : global);\n\n// Capture the original console just once\nconst ORIGINAL_CONSOLE = GLOBAL.console || {};\nconst ORIGINALS = {};\nlet INSTALLED = false;\nlet CURRENT_LEVEL = null;\n\n// Default levels\nconst DEFAULT_DEV_LEVEL = 'debug';\nconst DEFAULT_PROD_LEVEL = 'warn'; // keep warn + error by default\n\n// Helper: normalize/parse level\nfunction parseLevel(level) {\n if (typeof level === 'number') {\n // Clamp to bounds\n const min = LEVELS.silent;\n const max = LEVELS.trace;\n return Math.min(Math.max(level, min), max);\n }\n if (typeof level === 'string') {\n const key = level.toLowerCase();\n if (Object.prototype.hasOwnProperty.call(LEVELS, key)) {\n return LEVELS[key];\n }\n }\n return null;\n}\n\n// Helper: detect override via URLSearchParams\nfunction getUrlLogLevel() {\n if (!isBrowser || typeof location === 'undefined' || !location.search) return null;\n try {\n const params = new URLSearchParams(location.search);\n const keys = ['logLevel', 'loglevel', 'mojoLog'];\n for (const k of keys) {\n const v = params.get(k);\n if (v != null) {\n const parsed = parseLevel(v);\n if (parsed !== null) return parsed;\n }\n }\n } catch {\n // ignore\n }\n return null;\n}\n\n// Helper: detect override via localStorage\nfunction getStoredLogLevel() {\n if (!isBrowser || !('localStorage' in GLOBAL)) return null;\n try {\n const v = GLOBAL.localStorage.getItem('MOJO_LOG_LEVEL');\n if (v != null) {\n const parsed = parseLevel(v);\n if (parsed !== null) return parsed;\n }\n } catch {\n // ignore storage errors\n }\n return null;\n}\n\nfunction storeLogLevel(levelNumberOrName) {\n if (!isBrowser || !('localStorage' in GLOBAL)) return;\n try {\n const key = typeof levelNumberOrName === 'string'\n ? levelNumberOrName\n : levelNumberOrName === null\n ? null\n : Object.entries(LEVELS).find(([, num]) => num === levelNumberOrName)?.[0] ?? null;\n if (key) {\n GLOBAL.localStorage.setItem('MOJO_LOG_LEVEL', key);\n } else {\n GLOBAL.localStorage.removeItem('MOJO_LOG_LEVEL');\n }\n } catch {\n // ignore storage errors\n }\n}\n\n// Build a wrapped method that checks the current level before calling through\nfunction makeWrapper(methodName, methodLevel) {\n const original = ORIGINALS[methodName] || ORIGINAL_CONSOLE[methodName] || (() => {});\n return function wrappedConsoleMethod(...args) {\n // If the method is allowed at the current level, call through\n if (CURRENT_LEVEL >= methodLevel) {\n return original.apply(ORIGINAL_CONSOLE, args);\n }\n // Otherwise, noop\n return undefined;\n };\n}\n\n// Special wrapper for assert: only logs when assertion fails\nfunction makeAssertWrapper() {\n const original = ORIGINALS.assert || ORIGINAL_CONSOLE.assert || (() => {});\n return function wrappedAssert(condition, ...args) {\n // Only logs when condition is falsy; treat failed assert like error-level\n if (!condition) {\n if (CURRENT_LEVEL >= LEVELS.error) {\n return original.apply(ORIGINAL_CONSOLE, [condition, ...args]);\n }\n return undefined;\n }\n // When assertion passes, no-op\n return undefined;\n };\n}\n\nfunction determineInitialLevel(explicitLevel) {\n // 1) Explicit install option\n const explicit = parseLevel(explicitLevel);\n if (explicit !== null) return explicit;\n\n // 2) URL override\n const urlLevel = getUrlLogLevel();\n if (urlLevel !== null) return urlLevel;\n\n // 3) localStorage override\n const storedLevel = getStoredLogLevel();\n if (storedLevel !== null) return storedLevel;\n\n // 4) Environment default\n return parseLevel(isDev ? DEFAULT_DEV_LEVEL : DEFAULT_PROD_LEVEL);\n}\n\n// Construct a patched console that respects CURRENT_LEVEL\nfunction buildPatchedConsole() {\n const patched = { ...ORIGINAL_CONSOLE };\n\n // Save originals once\n const methodLevels = {\n // Critical\n error: LEVELS.error,\n warn: LEVELS.warn,\n\n // Informational\n info: LEVELS.info,\n log: LEVELS.info,\n dir: LEVELS.info,\n table: LEVELS.info,\n\n // Verbose / Debug\n debug: LEVELS.debug,\n group: LEVELS.debug,\n groupCollapsed: LEVELS.debug,\n groupEnd: LEVELS.debug,\n time: LEVELS.debug,\n timeEnd: LEVELS.debug,\n timeLog: LEVELS.debug,\n trace: LEVELS.trace,\n };\n\n // Capture originals and build wrappers\n for (const name of Object.keys(methodLevels)) {\n ORIGINALS[name] = ORIGINAL_CONSOLE[name] || (() => {});\n patched[name] = makeWrapper(name, methodLevels[name]);\n }\n\n // Special-case assert\n ORIGINALS.assert = ORIGINAL_CONSOLE.assert || (() => {});\n patched.assert = makeAssertWrapper();\n\n // Preserve any other console methods as-is (clear, profile, count, etc.)\n return patched;\n}\n\nconst ConsoleSilencer = {\n // Install the silencer (idempotent)\n install(options = {}) {\n if (INSTALLED) {\n // Already installed; update level if provided\n if (options && typeof options.level !== 'undefined') {\n this.setLevel(options.level, { persist: !!options.persist });\n }\n return this;\n }\n\n if (!GLOBAL || !ORIGINAL_CONSOLE) {\n // No console available; nothing to do\n INSTALLED = true; // Prevent re-entry\n return this;\n }\n\n CURRENT_LEVEL = determineInitialLevel(options.level);\n\n const patched = buildPatchedConsole();\n // Replace the global console\n GLOBAL.console = patched;\n\n INSTALLED = true;\n\n // Expose for quick manual access if desired\n GLOBAL.MOJOConsoleSilencer = this;\n\n return this;\n },\n\n // Uninstall and restore the original console\n uninstall() {\n if (!INSTALLED) return this;\n\n try {\n GLOBAL.console = ORIGINAL_CONSOLE;\n } catch {\n // ignore\n }\n INSTALLED = false;\n return this;\n },\n\n // Set current level at runtime; accepts string or number\n // levels: 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace'\n setLevel(level, { persist = false } = {}) {\n const parsed = parseLevel(level);\n if (parsed === null) return this; // ignore invalid level\n CURRENT_LEVEL = parsed;\n if (persist) {\n storeLogLevel(level);\n }\n return this;\n },\n\n // Get the current numeric level\n getLevel() {\n return CURRENT_LEVEL;\n },\n\n // Get the current level name (best-effort)\n getLevelName() {\n const entry = Object.entries(LEVELS).find(([, num]) => num === CURRENT_LEVEL);\n return entry ? entry[0] : null;\n },\n\n // Convenience helpers\n criticalOnly({ persist = false } = {}) {\n return this.setLevel('warn', { persist });\n },\n\n errorsOnly({ persist = false } = {}) {\n return this.setLevel('error', { persist });\n },\n\n silent({ persist = false } = {}) {\n return this.setLevel('silent', { persist });\n },\n\n verbose({ persist = false } = {}) {\n return this.setLevel(isDev ? 'debug' : 'info', { persist });\n },\n\n allowAll({ persist = false } = {}) {\n return this.setLevel('trace', { persist });\n },\n\n // Run a block with a temporary level, then restore\n withTemporaryLevel(level, fn) {\n const prev = CURRENT_LEVEL;\n const parsed = parseLevel(level);\n if (parsed === null || typeof fn !== 'function') return fn?.();\n CURRENT_LEVEL = parsed;\n try {\n return fn();\n } finally {\n CURRENT_LEVEL = prev;\n }\n },\n\n // Expose levels map for consumers\n LEVELS,\n};\n\n// Optional: convenience named export alias\nexport default ConsoleSilencer;\nexport const installConsoleSilencer = (options) => ConsoleSilencer.install(options);","/**\n * GroupSearchView - Hierarchical tree-view search component\n * Extends SimpleSearchView to support parent/child relationships\n * Displays items in a tree structure based on parent property\n */\n\nimport SimpleSearchView from './SimpleSearchView.js';\n\nclass GroupSearchView extends SimpleSearchView {\n constructor(options = {}) {\n super({\n ...options,\n className: `group-search-view ${options.className || ''}`.trim()\n });\n\n // Tree-specific configuration\n this.showKind = options.showKind !== undefined ? options.showKind : true;\n this.parentField = options.parentField || 'parent';\n this.kindField = options.kindField || 'kind';\n this.treeData = []; // Processed hierarchical data\n this.flattenedItems = []; // Flattened tree for display\n \n // Visual customization\n this.showLines = options.showLines !== undefined ? options.showLines : true;\n }\n\n /**\n * Build tree hierarchy from flat list\n */\n buildTreeHierarchy(items) {\n if (!items || items.length === 0) {\n return [];\n }\n\n // Create a map of items by ID for quick lookup\n const itemsById = new Map();\n \n // First pass: add all items and extract parent objects\n items.forEach(item => {\n if (!itemsById.has(item.id)) {\n itemsById.set(item.id, {\n ...item,\n children: [],\n level: 0,\n hasChildren: false\n });\n }\n \n // If this item has a parent object, add the parent too\n const parentObj = item[this.parentField];\n if (parentObj && parentObj.id && !itemsById.has(parentObj.id)) {\n itemsById.set(parentObj.id, {\n ...parentObj,\n children: [],\n level: 0,\n hasChildren: false\n });\n }\n });\n\n const rootItems = [];\n\n // Build parent-child relationships\n itemsById.forEach((treeItem, itemId) => {\n const originalItem = items.find(i => i.id === itemId) || treeItem;\n const parentId = originalItem[this.parentField]?.id;\n\n if (parentId && itemsById.has(parentId)) {\n const parent = itemsById.get(parentId);\n parent.children.push(treeItem);\n parent.hasChildren = true;\n } else {\n rootItems.push(treeItem);\n }\n });\n\n // Calculate levels recursively\n const calculateLevels = (nodes, level = 0) => {\n nodes.forEach(node => {\n node.level = level;\n if (node.children.length > 0) {\n node.children.sort((a, b) => (a.name || '').localeCompare(b.name || ''));\n calculateLevels(node.children, level + 1);\n }\n });\n };\n\n rootItems.sort((a, b) => (a.name || '').localeCompare(b.name || ''));\n calculateLevels(rootItems);\n\n return rootItems;\n }\n\n /**\n * Flatten tree structure for rendering\n * Tracks ancestor \"last child\" flags for proper line rendering\n */\n flattenTree(nodes, result = [], ancestorLastFlags = []) {\n nodes.forEach((node, index) => {\n node._isLastChild = index === nodes.length - 1;\n node._ancestorLastFlags = [...ancestorLastFlags];\n \n // Check if this node is the last descendant in its branch\n // (all ancestors are last children AND this node is last child with no children)\n const allAncestorsLast = ancestorLastFlags.every(flag => flag);\n node._isLastDescendant = allAncestorsLast && node._isLastChild && \n (!node.children || node.children.length === 0);\n \n result.push(node);\n \n if (node.children && node.children.length > 0) {\n const newFlags = [...ancestorLastFlags, node._isLastChild];\n this.flattenTree(node.children, result, newFlags);\n }\n });\n \n return result;\n }\n\n /**\n * Second pass: compute which vertical lines should continue for each item\n * \n * Vertical line at segment s shows if there are more siblings coming at level s+1\n * We need to look ahead until we find an item at level s+1 or shallower\n */\n computeVerticalLines(flatList) {\n for (let i = 0; i < flatList.length; i++) {\n const item = flatList[i];\n item._continueVertical = [];\n \n for (let s = 0; s < item.level; s++) {\n // Look ahead to find next item at level s+1 or shallower\n let foundSibling = false;\n for (let j = i + 1; j < flatList.length; j++) {\n const futureItem = flatList[j];\n if (futureItem.level === s + 1) {\n // Found a sibling at this level - vertical should continue\n foundSibling = true;\n break;\n } else if (futureItem.level <= s) {\n // Went back to ancestor level or shallower - no more siblings\n break;\n }\n // Keep searching if futureItem.level > s+1 (still in deeper subtree)\n }\n item._continueVertical[s] = foundSibling;\n }\n }\n }\n\n /**\n * Update filtered items with tree structure\n */\n updateFilteredItems() {\n if (!this.collection) {\n this.filteredItems = [];\n this.treeData = [];\n this.flattenedItems = [];\n return;\n }\n\n // Server-side filtering is handled in performSearch() (from parent class)\n const items = this.collection.toJSON();\n this.treeData = this.buildTreeHierarchy(items);\n this.flattenedItems = this.flattenTree(this.treeData);\n \n // Second pass: compute vertical line continuation\n this.computeVerticalLines(this.flattenedItems);\n \n // Set filteredItems to flattened tree for parent class compatibility\n this.filteredItems = this.flattenedItems;\n\n this.updateResultsView();\n }\n\n /**\n * Get tree-specific item template\n */\n getDefaultItemTemplate() {\n return `\n <div class=\"tree-item-content\">\n <div class=\"tree-item-name\">{{name}}</div>\n {{#showKind}}\n <div class=\"tree-item-kind\">{{kind}}</div>\n {{/showKind}}\n </div>\n `;\n }\n\n /**\n * Process item template with tree structure\n */\n processItemTemplate(item) {\n // Process the base template\n let content = this.itemTemplate;\n content = content.replace(/\\{\\{(\\w+)\\}\\}/g, (match, prop) => {\n if (prop === 'showKind') {\n return this.showKind ? 'true' : '';\n }\n return this.getNestedValue(item, prop) || '';\n });\n\n // Handle conditional sections for kind\n if (this.showKind) {\n content = content.replace(/\\{\\{#showKind\\}\\}(.*?)\\{\\{\\/showKind\\}\\}/gs, '$1');\n } else {\n content = content.replace(/\\{\\{#showKind\\}\\}.*?\\{\\{\\/showKind\\}\\}/gs, '');\n }\n\n // Build tree line segments\n // For an item at level N, we need N segments (one for each ancestor level)\n let lineSegments = '';\n if (this.showLines && item.level > 0) {\n for (let i = 0; i < item.level; i++) {\n const isLastSegment = (i === item.level - 1);\n \n if (isLastSegment) {\n // This segment connects directly to this item\n // Use _isLastChild to determine if this is └ or ├\n const segClass = item._isLastChild ? 'tree-seg tree-seg-last' : 'tree-seg tree-seg-mid';\n lineSegments += `<span class=\"${segClass}\"></span>`;\n } else {\n // Ancestor segment - vertical line continues if next item is deeper than this level\n const continueVertical = item._continueVertical && item._continueVertical[i];\n if (continueVertical) {\n lineSegments += `<span class=\"tree-seg tree-seg-vert\"></span>`;\n } else {\n lineSegments += `<span class=\"tree-seg\"></span>`;\n }\n }\n }\n }\n\n // Classes for the wrapper\n const hasChildren = item.hasChildren ? ' has-children' : '';\n const isLastChild = item._isLastChild ? ' is-last-child' : '';\n\n // Wrap in tree structure\n return `\n <div class=\"tree-item-wrapper${hasChildren}${isLastChild}\" data-tree-level=\"${item.level}\">\n <div class=\"tree-lines\">\n ${lineSegments}\n </div>\n <div class=\"tree-item-body flex-grow-1\">\n ${content}\n </div>\n </div>\n `;\n }\n\n /**\n * Get all root items\n */\n getRootItems() {\n return this.treeData;\n }\n\n /**\n * Get children of a specific node\n */\n getNodeChildren(nodeId) {\n const findNode = (nodes, targetId) => {\n for (const node of nodes) {\n if (node.id === targetId) {\n return node.children;\n }\n if (node.children.length > 0) {\n const found = findNode(node.children, targetId);\n if (found) return found;\n }\n }\n return null;\n };\n\n return findNode(this.treeData, nodeId) || [];\n }\n}\n\nexport default GroupSearchView;\n","/**\n * Sidebar - Simple sidebar navigation component for MOJO framework\n * Provides easy menu switching and dynamic configuration\n */\n\nimport View from '@core/View.js';\nimport GroupSearchView from './GroupSearchView.js';\nimport {GroupList, Group} from '@core/models/Group.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\n\nclass Sidebar extends View {\n constructor(options = {}) {\n super({\n tagName: 'nav',\n className: 'sidebar',\n id: 'sidebar',\n ...options\n });\n\n this.menus = new Map();\n this.activeMenuName = null;\n this.currentRoute = null;\n this.showToggle = options.showToggle; // Default to true\n this.isCollapsed = false;\n this.sidebarTheme = options.theme || 'sidebar-light';\n this.customView = null;\n if (this.options.groupHeader) this.groupHeader = this.options.groupHeader;\n\n // Group selector configuration\n // 'inline' (default) - replaces sidebar view\n // 'dialog' - opens in a modal dialog like TopNav\n this.groupSelectorMode = options.groupSelectorMode || 'inline';\n this.groupSelectorDialog = null;\n // Apply sidebar theme\n if (this.sidebarTheme) {\n this.addClass(this.sidebarTheme);\n }\n\n // Initialize menus\n this.initializeMenus(options);\n\n // Setup route change listeners like TopNav\n this.setupRouteListeners();\n\n // Auto-collapse on mobile if specified\n if (options.autoCollapseMobile !== false) {\n this.setupResponsiveBehavior();\n }\n }\n\n groupHeader = `\n {{#group.parent}}\n <div class=\"sidebar-parent-bar\" data-action=\"select-group-parent\">\n <div class=\"parent-info\">\n <span class=\"parent-label\">{{group.parent.kind}}:</span>\n <span class=\"parent-name collapsed-hidden\">{{group.parent.name}}</span>\n </div>\n <i class=\"bi bi-chevron-down parent-expand collapsed-hidden\"></i>\n </div>\n {{/group.parent}}\n <div class=\"sidebar-selected-group-row\" data-action=\"show-group-search\">\n <div class=\"selected-group-info\">\n <div class='selected-group-name collapsed-hidden'>{{group.name}}</div>\n <div class='selected-group-meta collapsed-hidden'>\n <span class=\"selected-group-kind\">{{group.kind}}</span>\n </div>\n </div>\n <i class=\"bi bi-chevron-down selected-group-chevron collapsed-hidden\"></i>\n </div>\n `;\n\n /**\n * Initialize sidebar and auto-switch to correct menu based on current route\n */\n async onInit() {\n await super.onInit();\n\n // Get current route from router\n const app = this.getApp();\n const router = app?.router;\n\n if (router) {\n const currentPath = router.getCurrentPath();\n if (currentPath) {\n this.autoSwitchToMenuForRoute(currentPath);\n }\n }\n\n // Initialize tooltips for nav items\n this.initializeTooltips();\n\n this.searchView = new GroupSearchView({\n noAppend: true,\n showExitButton: true,\n headerText: \"Select Group\",\n containerId: \"sidebar-search-container\",\n Collection: GroupList,\n itemTemplate: `\n <div class=\"p-3 border-bottom\">\n <div class=\"fw-semibold text-dark\">{{name}}</div>\n <small class=\"text-muted\">#{{id}} {{kind}}</small>\n </div>\n `\n });\n this.addChild(this.searchView);\n this.searchView.on(\"item:selected\", (evt) => {\n console.log(evt);\n this.getApp().setActiveGroup(evt.model);\n });\n this.searchView.on(\"exit\", (item) => {\n console.log(item);\n this.hideGroupSearch();\n });\n }\n\n showGroupSearch() {\n if (this.groupSelectorMode === 'dialog') {\n this.showGroupSearchDialog();\n } else {\n // Inline mode (default)\n this.setClass('sidebar');\n this.showSearch = true;\n this.render();\n }\n }\n\n hideGroupSearch() {\n if (this.groupSelectorMode === 'dialog') {\n if (this.groupSelectorDialog) {\n this.groupSelectorDialog.hide();\n }\n } else {\n // Inline mode\n this.setClass('sidebar');\n this.showSearch = false;\n this.render();\n }\n }\n\n onActionShowGroupSearch() {\n this.showGroupSearch();\n }\n\n async onActionSelectGroupParent() {\n // select-group-parent\n const group = this.getApp().activeGroup;\n const result = await Dialog.confirm(`Are you sure you want to navigate to the '${group.get(\"parent.name\")}'?`);\n if (result) {\n this.getApp().showLoading();\n let parent = new Group({id: group.get(\"parent.id\")});\n await parent.fetch();\n this.getApp().setActiveGroup(parent);\n this.getApp().hideLoading();\n }\n }\n\n /**\n * Show group selector in a dialog (like TopNav)\n */\n async showGroupSearchDialog() {\n // Create or reuse collection instance (like GroupSelectorButton does)\n const collection = new GroupList();\n\n // Create GroupSearchView instance matching GroupSelectorButton pattern\n const searchView = new GroupSearchView({\n Collection: GroupList,\n collection: collection, // Pass the collection instance\n searchFields: ['name'],\n headerText: null,\n searchPlaceholder: \"Search groups...\",\n headerIcon: null,\n maxHeight: Math.min(600, window.innerHeight - 200),\n showExitButton: false,\n showKind: true, // Show kind badges (default: true)\n parentField: 'parent', // Field containing parent object\n kindField: 'kind', // Field containing kind/type\n autoExpandRoot: true, // Auto-expand root items (default: true)\n autoExpandAll: false, // Auto-expand all nodes (default: false)\n indentSize: 20, // Pixels per level (default: 20)\n showLines: true,\n });\n\n // Create dialog\n this.groupSelectorDialog = new Dialog({\n body: searchView,\n size: 'md',\n header: null,\n noBodyPadding: true,\n scrollable: false,\n buttons: [],\n closeButton: true\n });\n\n // Listen for item selection\n searchView.on('item:selected', (evt) => {\n console.log(evt);\n this.getApp().setActiveGroup(evt.model);\n if (this.groupSelectorDialog) {\n this.groupSelectorDialog.hide();\n }\n });\n\n // Clean up dialog reference when closed\n this.groupSelectorDialog.on('hidden', () => {\n this.groupSelectorDialog.destroy();\n this.groupSelectorDialog = null;\n });\n\n // Render and show the dialog\n await this.groupSelectorDialog.render(true, document.body);\n this.groupSelectorDialog.show();\n }\n\n /**\n * Find and switch to the menu that contains the given route\n */\n autoSwitchToMenuForRoute(route) {\n // Search through all menus to find one that contains this route\n for (const [menuName, menuConfig] of this.menus) {\n if (menuConfig.groupKind && !this.getApp().activeGroup)\n continue;\n if (this.menuContainsRoute(menuConfig, route)) {\n // Switch to this menu\n this._setActiveMenu(menuName);\n this.currentRoute = route;\n\n // Clear all active states and set new active item\n this.clearAllActiveStates();\n this.setActiveItemByRoute(route);\n\n // Re-render to show changes\n this.render();\n\n console.log(`Auto-switched to menu '${menuName}' for route '${route}'`);\n\n // Emit event for any listeners\n this.emit('menu-auto-switched', {\n menuName,\n route,\n config: menuConfig,\n sidebar: this\n });\n\n return true;\n }\n }\n\n return false; // No menu found for this route\n }\n\n /**\n * Clear active state from all menu items in all menus\n */\n clearAllActiveStates() {\n for (const [menuName, menuConfig] of this.menus) {\n for (const item of menuConfig.items || []) {\n item.active = false;\n if (item.children) {\n for (const child of item.children) {\n child.active = false;\n }\n }\n }\n }\n }\n\n /**\n * Set active state for item matching the given route\n */\n setActiveItemByRoute(route) {\n const normalizeRoute = (r) => {\n if (!r) return '/';\n // Decode URL-encoded characters (like %2F -> /)\n const decoded = decodeURIComponent(r);\n return decoded.startsWith('/') ? decoded : `/${decoded}`;\n };\n\n const targetRoute = normalizeRoute(route);\n\n // Search through all menus\n for (const [menuName, menuConfig] of this.menus) {\n if (menuConfig.groupKind && !this.getApp().activeGroup)\n continue;\n for (const item of menuConfig.items || []) {\n // Check main item\n if (item.route) {\n const itemRoute = normalizeRoute(item.route);\n if (this.routesMatch(targetRoute, itemRoute)) {\n item.active = true;\n this.activeMenuItem = item;\n return true;\n }\n }\n\n // Check children\n if (item.children) {\n for (const child of item.children) {\n if (child.route) {\n const childRoute = normalizeRoute(child.route);\n if (this.routesMatch(targetRoute, childRoute)) {\n child.active = true;\n item.active = true; // Parent also active\n return true;\n }\n }\n }\n }\n }\n }\n\n return false;\n }\n\n /**\n * Check if a menu contains a specific route in its items or children\n */\n menuContainsRoute(menuConfig, route) {\n const normalizeRoute = (r) => {\n if (!r) return '/';\n // Decode URL-encoded characters (like %2F -> /)\n const decoded = decodeURIComponent(r);\n return decoded.startsWith('/') ? decoded : `/${decoded}`;\n };\n\n const targetRoute = normalizeRoute(route);\n\n // Check each item in the menu\n for (const item of menuConfig.items || []) {\n // Check main item\n if (item.route) {\n const itemRoute = normalizeRoute(item.route);\n if (this.routesMatch(targetRoute, itemRoute)) {\n return true;\n }\n }\n\n // Check children\n if (item.children) {\n for (const child of item.children) {\n if (child.route) {\n const childRoute = normalizeRoute(child.route);\n if (this.routesMatch(targetRoute, childRoute)) {\n return true;\n }\n }\n }\n }\n }\n\n return false;\n }\n\n /**\n * Check if two routes match (using same logic as isItemActive)\n */\n routesMatch(currentRoute, itemRoute) {\n return this.getApp().router.doRoutesMatch(currentRoute, itemRoute);\n }\n\n getTemplate() {\n if (this.customView) {\n return '<div class=\"sidebar-container\" id=\"sidebar-custom-view-container\"></div>';\n }\n if (this.showSearch) return this.getSearchTemplate();\n return this.getMenuTemplate();\n }\n\n getSearchTemplate() {\n return `\n <div class=\"sidebar-container\" id=\"sidebar-search-container\">\n </div>\n `;\n }\n\n getMenuTemplate() {\n return `\n <div class=\"sidebar-container\">\n {{#data.currentMenu}}\n <!-- Header -->\n {{#header}}\n <div class=\"sidebar-header\">\n {{{header}}}\n {{#showToggle}}\n <button class=\"sidebar-toggle\" data-action=\"toggle-sidebar\"\n aria-label=\"Toggle Sidebar\">\n <i class=\"bi bi-chevron-left toggle-icon\"></i>\n <i class=\"bi bi-chevron-right toggle-icon\"></i>\n </button>\n {{/showToggle}}\n </div>\n {{/header}}\n\n <!-- Navigation Items -->\n <div class=\"sidebar-body\">\n <ul class=\"nav nav-pills flex-column sidebar-nav\" id=\"sidebar-nav-menu\">\n {{#items}}\n {{>nav-item}}\n {{/items}}\n </ul>\n </div>\n\n <!-- Footer -->\n {{#footer}}\n <div class=\"sidebar-footer\">\n {{{footer}}}\n </div>\n {{/footer}}\n {{/data.currentMenu}}\n\n {{^data.currentMenu}}\n <div class=\"sidebar-empty\">\n <p class=\"text-danger text-center\">No menu configured</p>\n </div>\n {{/data.currentMenu}}\n </div>\n `;\n }\n\n /**\n * Get template partials for rendering\n */\n getPartials() {\n return {\n 'nav-item': `\n {{#isDivider}}\n {{>nav-divider}}\n {{/isDivider}}\n {{#isSpacer}}\n {{>nav-spacer}}\n {{/isSpacer}}\n {{#isLabel}}\n {{>nav-label}}\n {{/isLabel}}\n\n {{^isDivider}}\n {{^isSpacer}}\n {{^isLabel}}\n <li class=\"nav-item\">\n {{#hasChildren}}\n <!-- Item with submenu -->\n <a class=\"nav-link {{#active}}active{{/active}} has-children collapsed\"\n data-bs-toggle=\"collapse\"\n href=\"#collapse-{{id}}\"\n role=\"button\"\n aria-expanded=\"{{#active}}true{{/active}}{{^active}}false{{/active}}\"\n data-action=\"toggle-submenu\">\n {{#icon}}<i class=\"{{icon}} me-2\"></i>{{/icon}}\n <span class=\"nav-text\">{{text}}</span>\n {{#badge}}\n <span class=\"{{badge.class}} ms-auto\">{{badge.text}}</span>\n {{/badge}}\n <i class=\"bi bi-chevron-down nav-arrow ms-auto\"></i>\n </a>\n <div class=\"collapse {{#active}}show{{/active}}\" id=\"collapse-{{id}}\" data-bs-parent=\"#sidebar-nav-menu\">\n <ul class=\"nav flex-column nav-submenu\">\n {{#children}}\n <li class=\"nav-item\">\n <a class=\"nav-link {{#active}}active{{/active}}\"\n {{#action}}data-action=\"{{action}}\"{{/action}}\n {{#href}}href=\"{{href}}\"{{/href}}>\n {{#icon}}<i class=\"{{icon}} me-2\"></i>{{/icon}}\n <span class=\"nav-text\">{{text}}</span>\n {{#badge}}\n <span class=\"{{badge.class}} ms-auto\">{{badge.text}}</span>\n {{/badge}}\n </a>\n </li>\n {{/children}}\n </ul>\n </div>\n {{/hasChildren}}\n {{^hasChildren}}\n <!-- Simple item -->\n <a class=\"nav-link {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}\"\n {{#action}}{{^disabled}}data-action=\"{{action}}\"{{/disabled}}{{/action}}\n {{#href}}{{^disabled}}href=\"{{href}}\"{{/disabled}}{{/href}}>\n {{#icon}}<i class=\"{{icon}} me-2\"></i>{{/icon}}\n <span class=\"nav-text\">{{text}}</span>\n {{#badge}}\n <span class=\"{{badge.class}} ms-auto\">{{badge.text}}</span>\n {{/badge}}\n </a>\n {{/hasChildren}}\n </li>\n {{/isLabel}}\n {{/isSpacer}}\n {{/isDivider}}\n `,\n 'nav-divider': `\n <li class=\"nav-divider-item\">\n <hr class=\"nav-divider-line\">\n </li>\n `,\n 'nav-spacer': `\n <li class=\"nav-spacer-item\"></li>\n `,\n 'nav-label': `\n <li class=\"nav-item {{className}}\">\n <div class=\"nav-text px-3\">{{text}}</div>\n </li>\n `\n };\n }\n\n getGroupHeader() {\n return this.groupHeader;\n }\n\n /**\n * Add a menu configuration\n */\n addMenu(name, config) {\n if (config.groupKind && !config.header) {\n config.header = this.getGroupHeader();\n }\n\n this.menus.set(name, {\n name,\n groupKind: config.groupKind || null,\n header: config.header || null,\n footer: config.footer || null,\n items: config.items || [],\n data: config.data || {},\n className: config.className || \"sidebar sidebar-dark\"\n });\n\n\n // Set as active if it's the first menu\n if (!this.activeMenuName) {\n this._setActiveMenu(name);\n }\n\n return this;\n }\n\n _setActiveMenu(name) {\n this.showSearch = false;\n this.activeMenuName = name;\n const config = this.getCurrentMenuConfig();\n if (config.className) {\n this.setClass(config.className);\n } else {\n this.setClass('sidebar');\n }\n }\n\n /**\n * Set the active menu\n */\n async setActiveMenu(name) {\n if (!this.menus.has(name)) {\n console.warn(`Menu '${name}' not found`);\n return this;\n }\n\n const menuConfig = this.menus.get(name);\n\n if (menuConfig.groupKind) {\n this.lastGroupMenu = menuConfig;\n // Handle group kind logic here\n if (!this.getApp().activeGroup) {\n this.showGroupSearch();\n return;\n }\n }\n\n this._setActiveMenu(name);\n await this.render();\n // Emit event\n this.emit('menu-changed', {\n menuName: name,\n config: menuConfig,\n sidebar: this\n });\n\n return this;\n }\n\n getGroupMenu(group) {\n if (!group) {\n console.warn('No group provided');\n return null;\n }\n // Find menu by group.kind\n let targetMenu = this.lastGroupMenu;\n let anyGroupMenu = null;\n if (group._.kind) {\n for (const [menuName, menuConfig] of this.menus) {\n // Check if groupKind matches\n const matches = this._groupKindMatches(menuConfig.groupKind, group._.kind);\n\n if (matches) {\n targetMenu = menuConfig;\n break;\n } else if (menuConfig.groupKind === 'any') {\n anyGroupMenu = menuConfig;\n }\n }\n }\n\n if (!targetMenu) {\n return anyGroupMenu;\n }\n return targetMenu;\n }\n\n /**\n * Check if a groupKind matches the group's kind\n * Supports both single string and array of strings\n * @param {string|string[]} groupKind - Single kind or array of kinds\n * @param {string} kind - The group's kind to match\n * @returns {boolean} True if matches\n */\n _groupKindMatches(groupKind, kind) {\n if (!groupKind || !kind) return false;\n\n // Handle array of kinds\n if (Array.isArray(groupKind)) {\n return groupKind.includes(kind);\n }\n\n // Handle single kind string\n return groupKind === kind;\n }\n\n showMenuForGroup(group) {\n if (!group) {\n console.warn('No group provided');\n return;\n }\n // Find menu by group.kind\n let targetMenu = this.getGroupMenu(group);\n\n if (!targetMenu) {\n console.warn(`No menu found for group kind: ${group.kind}`);\n return;\n }\n this._setActiveMenu(targetMenu.name);\n this.render();\n // Emit event\n this.emit('menu-changed', {\n menuName: targetMenu.name,\n config: targetMenu,\n sidebar: this\n });\n return this;\n }\n\n /**\n * Get menu configuration\n */\n getMenuConfig(name) {\n return this.menus.get(name) || null;\n }\n\n /**\n * Get current active menu configuration\n */\n getCurrentMenuConfig() {\n return this.activeMenuName ? this.menus.get(this.activeMenuName) : null;\n }\n\n /**\n * Update menu configuration\n */\n updateMenu(name, updates) {\n const menu = this.menus.get(name);\n if (!menu) {\n console.warn(`Menu '${name}' not found`);\n return this;\n }\n\n // Deep merge updates\n Object.assign(menu, updates);\n\n // Re-render if this is the active menu\n if (this.activeMenuName === name) {\n this.render();\n }\n\n return this;\n }\n\n /**\n * Remove a menu\n */\n removeMenu(name) {\n this.menus.delete(name);\n\n // If this was the active menu, switch to another or clear\n if (this.activeMenuName === name) {\n const remainingMenus = Array.from(this.menus.keys());\n this.activeMenuName = remainingMenus.length > 0 ? remainingMenus[0] : null;\n this.render();\n }\n\n return this;\n }\n\n /**\n * Get view data for template rendering\n */\n async onBeforeRender() {\n const currentMenu = this.getCurrentMenuConfig();\n\n if (!currentMenu) {\n return { currentMenu: null };\n }\n\n let subData = {\n version: this.getApp().version || null,\n group: this.getApp().activeGroup || null,\n user: this.getApp.activeUser || null\n };\n // Process menu data through template if it contains handlebars\n this.data = {\n currentMenu: {\n header: this.renderTemplateString(currentMenu.header || '', subData),\n footer: this.renderTemplateString(currentMenu.footer || '', subData),\n items: this.processNavItems(currentMenu.items, currentMenu.groupKind),\n data: currentMenu.data,\n showToggle: this.showToggle\n }\n\n };\n }\n\n async onAfterRender() {\n // Re-initialize tooltips after render, but only if collapsed\n if (this.isCollapsedState()) {\n // Small delay to ensure DOM is fully rendered\n setTimeout(() => this.initializeTooltips(), 50);\n } else {\n this.destroyTooltips();\n }\n }\n\n setCustomView(view) {\n if (this.customView) {\n this.removeChild(this.customView.id);\n }\n this.customView = view;\n if (view) {\n view.containerId = 'sidebar-custom-view-container';\n this.addChild(view);\n }\n this.render();\n return this;\n }\n\n clearCustomView() {\n if (this.customView) {\n this.removeChild(this.customView.id);\n this.customView = null;\n }\n this.render();\n return this;\n }\n\n /**\n * Process navigation items - add IDs, active states, and proper hrefs\n */\n processNavItems(items, groupKind) {\n const app = this.getApp();\n const activeUser = app?.activeUser;\n const activeGroup = app?.activeGroup;\n\n // Helper function to normalize and update route with group parameter\n const updateRouteWithGroup = (route) => {\n let normalizedRoute = route;\n \n // Convert path format (/forms) to query string format (?page=forms)\n if (route.startsWith('/') && !route.includes('?')) {\n const pageName = route.substring(1) || 'home';\n normalizedRoute = `?page=${pageName}`;\n }\n \n // Add group parameter if needed\n if (groupKind && activeGroup && activeGroup.id) {\n const separator = normalizedRoute.includes('?') ? '&' : '?';\n return `${normalizedRoute}${separator}group=${activeGroup.id}`;\n }\n return normalizedRoute;\n };\n\n return items.map((item, index) => {\n // Handle divider items\n if (item === \"\" || (typeof item === 'object' && item.divider)) {\n return {\n isDivider: true,\n id: `divider-${index}`\n };\n }\n\n // Handle spacer items\n if (typeof item === 'object' && item.spacer) {\n return {\n isSpacer: true,\n id: `spacer-${index}`\n };\n }\n\n const processedItem = { ...item };\n\n // Check permissions - skip item if user doesn't have required permissions\n if (processedItem.permissions) {\n if (!activeUser || !activeUser.hasPermission(processedItem.permissions)) {\n return null; // Will be filtered out\n }\n }\n\n // Check requiresGroupKind - skip item if group kind doesn't match\n if (processedItem.requiresGroupKind) {\n const groupKind = activeGroup?._.kind || activeGroup?.kind;\n if (!groupKind || !this._groupKindMatches(processedItem.requiresGroupKind, groupKind)) {\n return null; // Will be filtered out\n }\n }\n\n if (processedItem.kind === 'label') {\n processedItem.isLabel = true;\n if (!processedItem.id) {\n processedItem.id = `nav-label-${index}`;\n }\n return processedItem;\n }\n\n // Generate ID if not provided\n if (!processedItem.id) {\n processedItem.id = `nav-${index}`;\n }\n\n // Use route directly as href (like TopNav does)\n if (processedItem.route) {\n processedItem.href = updateRouteWithGroup(processedItem.route);\n } else if (processedItem.page) {\n // If only page is provided, convert to route format\n const baseRoute = processedItem.page.startsWith('/') ? processedItem.page : `/${processedItem.page}`;\n processedItem.href = updateRouteWithGroup(baseRoute);\n processedItem.route = processedItem.href; // Store for active matching\n }\n\n // Active state is already set on the item object, no need to calculate\n\n // Process children\n if (processedItem.children) {\n processedItem.children = processedItem.children.map(child => {\n const processedChild = { ...child };\n\n // Check permissions for child items\n if (processedChild.permissions && activeUser) {\n if (!activeUser.hasPermission(processedChild.permissions)) {\n return null; // Will be filtered out\n }\n }\n\n // Check requiresGroupKind for child items\n if (processedChild.requiresGroupKind) {\n const groupKind = activeGroup?._.kind || activeGroup?.kind;\n if (!groupKind || !this._groupKindMatches(processedChild.requiresGroupKind, groupKind)) {\n return null; // Will be filtered out\n }\n }\n\n // Use route directly as href\n if (processedChild.route) {\n processedChild.href = updateRouteWithGroup(processedChild.route);\n } else if (processedChild.page) {\n const baseRoute = processedChild.page.startsWith('/') ? processedChild.page : `/${processedChild.page}`;\n processedChild.href = updateRouteWithGroup(baseRoute);\n processedChild.route = processedChild.href;\n }\n\n // Active state is already set on the child object\n return processedChild;\n }).filter(child => child !== null); // Filter out permission-denied children\n\n // Update hasChildren flag after filtering children\n processedItem.hasChildren = !!(processedItem.children && processedItem.children.length > 0);\n } else {\n // Add hasChildren flag for template logic\n processedItem.hasChildren = false;\n }\n\n return processedItem;\n }).filter(item => item !== null); // Filter out permission-denied items\n }\n\n\n\n\n /**\n * Check if navigation item should be active (similar to TopNav)\n */\n isItemActive(item) {\n if (!item.route || !this.currentRoute) {\n return false;\n }\n\n const normalizeRoute = (route) => {\n if (!route) return '/';\n // Decode URL-encoded characters (like %2F -> /)\n const decoded = decodeURIComponent(route);\n return decoded.startsWith('/') ? decoded : `/${decoded}`;\n };\n\n const itemRoute = normalizeRoute(item.route);\n const currentRoute = normalizeRoute(this.currentRoute);\n\n if (itemRoute === '/' && currentRoute === '/') {\n return true;\n }\n\n if (itemRoute !== '/' && currentRoute !== '/') {\n return currentRoute.startsWith(itemRoute) || currentRoute === itemRoute;\n }\n\n return false;\n }\n\n /**\n * Update active item based on current route (like TopNav)\n */\n async updateActiveItem(route) {\n this.currentRoute = route;\n\n // Clear all active states and set new active item\n this.clearAllActiveStates();\n this.setActiveItemByRoute(route);\n\n await this.render();\n return this;\n }\n\n /**\n * Action handler: Toggle submenu\n */\n async handleActionToggleSubmenu(event, element) {\n const arrow = element.querySelector('.nav-arrow');\n if (arrow) {\n arrow.classList.toggle('rotated');\n }\n }\n\n /**\n * Action handler: Toggle sidebar collapsed/expanded state\n */\n async handleActionToggleSidebar(event, element) {\n this.toggleSidebar();\n }\n\n onActionShowGroupMenu(action, event, el) {\n this.setActiveMenu(\"group_default\");\n\n return false;\n }\n\n async onActionDefault(action, event, el) {\n const config = this.getCurrentMenuConfig();\n if (!config) return;\n\n // Helper to recursively search for action in items and children\n const findAndExecuteHandler = (items) => {\n for (const item of items) {\n if ((item.action == action) && item.handler) {\n item.handler(action, event, el, this.getApp());\n return true;\n }\n // Check children recursively\n if (item.children && item.children.length > 0) {\n if (findAndExecuteHandler(item.children)) {\n return true;\n }\n }\n }\n return false;\n };\n\n return findAndExecuteHandler(config.items);\n }\n\n /**\n * Get all menu names\n */\n getMenuNames() {\n return Array.from(this.menus.keys());\n }\n\n /**\n * Check if menu exists\n */\n hasMenu(name) {\n return this.menus.has(name);\n }\n\n /**\n * Clear all menus\n */\n clearMenus() {\n this.menus.clear();\n this.activeMenuName = null;\n this.render();\n return this;\n }\n\n /**\n * Set data for current menu\n */\n setMenuData(data) {\n const currentMenu = this.getCurrentMenuConfig();\n if (currentMenu) {\n currentMenu.data = { ...currentMenu.data, ...data };\n this.render();\n }\n return this;\n }\n\n /**\n * Get data for current menu\n */\n getMenuData() {\n const currentMenu = this.getCurrentMenuConfig();\n return currentMenu ? currentMenu.data : {};\n }\n\n /**\n * Setup listeners for route change events (like TopNav)\n */\n setupRouteListeners() {\n const app = this.getApp();\n if (app && app.events) {\n app.events.on([\"page:showing\"], (data) => {\n this.onRouteChanged(data);\n });\n app.events.on(\"group:changed\", (data) => {\n this.showMenuForGroup(data.group);\n });\n app.events.on(\"portal:user-changed\", (data) => {\n this.render();\n });\n }\n }\n\n /**\n * Handle route changed event - auto-switch menu and update active item\n */\n /**\n * Handle route changed event.\n *\n * Resolution order for \"which menu to show\":\n * 1. The menu that contains the new route (autoSwitchToMenuForRoute).\n * 2. The menu named by `page.sidebarMenu` (declared on the Page class).\n * 3. The first non-group menu registered — prevents \"last-menu-wins\" default.\n *\n * Pages that don't belong to any menu are called \"homeless\" pages.\n * Declare `sidebarMenu = 'menuName'` on the Page class to pin them to a menu:\n *\n * class SettingsPage extends Page {\n * sidebarMenu = 'default'; // show 'default' sidebar on this page\n * }\n */\n onRouteChanged(data) {\n if (data.page && data.page.route) {\n const route = data.page.route;\n if (this.activeMenuItem && this.routesMatch(route, this.activeMenuItem.route)) {\n return;\n }\n\n // 1. Try to auto-switch to the menu that contains this route\n const switchedMenu = this.autoSwitchToMenuForRoute(route);\n\n if (switchedMenu) {\n console.log(`Route changed to '${route}', auto-switched menu`);\n return;\n }\n\n // 2. \"Homeless\" page — route is not in any menu.\n // Check if the page class declares a preferred sidebarMenu name.\n const preferredMenu = data.page.sidebarMenu || data.page.options?.sidebarMenu || null;\n if (preferredMenu && this.menus.has(preferredMenu)) {\n this._setActiveMenu(preferredMenu);\n this.clearAllActiveStates();\n this.render();\n console.log(`Homeless route '${route}' — switched to page-declared sidebarMenu '${preferredMenu}'`);\n return;\n }\n\n // 3. Fall back to the first non-group menu so we never\n // accidentally land on the last-registered menu.\n let fallbackMenu = null;\n for (const [menuName, menuConfig] of this.menus) {\n if (!menuConfig.groupKind) {\n fallbackMenu = menuName;\n break;\n }\n }\n\n if (fallbackMenu && this.activeMenuName !== fallbackMenu) {\n this._setActiveMenu(fallbackMenu);\n console.log(`Homeless route '${route}' — fell back to first non-group menu '${fallbackMenu}'`);\n }\n\n // Always clear active states and re-render for homeless pages\n this.clearAllActiveStates();\n this.updateActiveItem(route);\n this.render();\n }\n }\n\n /**\n * Toggle sidebar between collapsed and expanded states\n */\n toggleSidebar() {\n const portalContainer = document.querySelector('.portal-container');\n if (!portalContainer) return;\n\n // Hide any visible tooltips before state change\n this.hideAllTooltips();\n\n const isCurrentlyCollapsed = portalContainer.classList.contains('collapse-sidebar');\n const isCurrentlyHidden = portalContainer.classList.contains('hide-sidebar');\n\n if (isCurrentlyHidden) {\n // Hidden -> Normal\n portalContainer.classList.remove('hide-sidebar');\n this.isCollapsed = false;\n this.destroyTooltips();\n } else if (isCurrentlyCollapsed) {\n // Collapsed -> Normal\n portalContainer.classList.remove('collapse-sidebar');\n this.isCollapsed = false;\n this.destroyTooltips();\n } else {\n // Normal -> Collapsed\n portalContainer.classList.add('collapse-sidebar');\n this.isCollapsed = true;\n // Initialize tooltips with delay to ensure DOM is ready\n setTimeout(() => this.initializeTooltips(), 150);\n }\n\n return this;\n }\n\n /**\n * Set sidebar state programmatically\n */\n setSidebarState(state) {\n const portalContainer = document.querySelector('.portal-container');\n if (!portalContainer) return this;\n\n // Remove all state classes first\n portalContainer.classList.remove('collapse-sidebar', 'hide-sidebar');\n\n switch (state) {\n case 'collapsed':\n portalContainer.classList.add('collapse-sidebar');\n this.isCollapsed = true;\n break;\n case 'hidden':\n portalContainer.classList.add('hide-sidebar');\n this.isCollapsed = false;\n break;\n case 'normal':\n default:\n this.isCollapsed = false;\n break;\n }\n\n // Handle tooltips based on state\n if (this.isCollapsed) {\n // Hide any visible tooltips first\n this.hideAllTooltips();\n // Initialize tooltips when collapsed\n setTimeout(() => this.initializeTooltips(), 100);\n } else {\n // Destroy tooltips when not collapsed\n this.destroyTooltips();\n }\n\n return this;\n }\n\n /**\n * Initialize tooltips for nav items when sidebar is collapsed\n */\n initializeTooltips() {\n // Clean up existing tooltips first\n this.destroyTooltips();\n\n // Only initialize tooltips in collapsed state\n if (!this.isCollapsedState()) {\n return this;\n }\n\n // Auto-generate tooltips from nav-text content\n const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link');\n\n navLinks.forEach((link) => {\n const navText = link.querySelector('.nav-text');\n\n if (navText && navText.textContent.trim()) {\n const tooltipText = navText.textContent.trim();\n\n // Set Bootstrap tooltip attributes\n link.setAttribute('data-bs-toggle', 'tooltip');\n link.setAttribute('data-bs-placement', 'right');\n link.setAttribute('data-bs-title', tooltipText);\n link.setAttribute('data-bs-container', 'body');\n\n // Initialize Bootstrap tooltip with better config\n if (window.bootstrap && window.bootstrap.Tooltip) {\n // Extract custom theme/size if specified\n const theme = link.getAttribute('data-tooltip-theme');\n const size = link.getAttribute('data-tooltip-size');\n\n // Build custom class list\n let customClass = '';\n if (theme) customClass += `tooltip-${theme} `;\n if (size) customClass += `tooltip-${size}`;\n\n // Build options object\n const tooltipOptions = {\n placement: 'right',\n container: 'body',\n trigger: 'hover',\n delay: { show: 500, hide: 100 },\n fallbackPlacements: ['top', 'bottom', 'left']\n };\n\n // Only add customClass if it has a value\n const trimmedClass = customClass.trim();\n if (trimmedClass) {\n tooltipOptions.customClass = trimmedClass;\n }\n\n const tooltip = new window.bootstrap.Tooltip(link, tooltipOptions);\n\n // Store tooltip instance for better management\n link._tooltipInstance = tooltip;\n\n // Add event listeners to prevent stuck tooltips\n link.addEventListener('click', () => {\n tooltip.hide();\n });\n\n link.addEventListener('blur', () => {\n tooltip.hide();\n });\n }\n }\n });\n\n // Add global event listeners to hide tooltips\n this.addTooltipHideListeners();\n\n return this;\n }\n\n destroyTooltips() {\n // Remove global tooltip hide listeners\n this.removeTooltipHideListeners();\n\n const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle=\"tooltip\"]');\n\n navLinks.forEach((link) => {\n // Use stored instance first, then try to get it\n const tooltipInstance = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);\n if (tooltipInstance) {\n // Force hide before dispose\n tooltipInstance.hide();\n tooltipInstance.dispose();\n }\n\n // Clean up stored reference\n delete link._tooltipInstance;\n\n // Remove tooltip attributes\n link.removeAttribute('data-bs-toggle');\n link.removeAttribute('data-bs-placement');\n link.removeAttribute('data-bs-title');\n link.removeAttribute('data-bs-container');\n });\n\n return this;\n }\n\n /**\n * Get current sidebar state\n */\n getSidebarState() {\n const portalContainer = document.querySelector('.portal-container');\n if (!portalContainer) return 'normal';\n\n if (portalContainer.classList.contains('hide-sidebar')) {\n return 'hidden';\n } else if (portalContainer.classList.contains('collapse-sidebar')) {\n return 'collapsed';\n } else {\n return 'normal';\n }\n }\n\n /**\n * Check if sidebar is collapsed\n */\n isCollapsedState() {\n return this.getSidebarState() === 'collapsed';\n }\n\n /**\n * Enable/disable toggle button\n */\n setToggleEnabled(enabled) {\n this.showToggle = enabled;\n this.render();\n return this;\n }\n\n /**\n * Initialize menus from options\n */\n initializeMenus(options) {\n if (options.menus) {\n for (const menu of options.menus) {\n this.addMenu(menu.name, menu);\n }\n } else if (options.menu) {\n options.menu.name = options.menu.name || \"default\";\n this.addMenu(options.menu.name, options.menu);\n }\n }\n\n /**\n * Add global listeners to hide tooltips when needed\n */\n addTooltipHideListeners() {\n // Hide tooltips on scroll\n this._tooltipScrollHandler = () => this.hideAllTooltips();\n this.element.addEventListener('scroll', this._tooltipScrollHandler, { passive: true });\n\n // Hide tooltips on route change\n this._tooltipRouteHandler = () => this.hideAllTooltips();\n const app = this.getApp();\n\n\n // Hide tooltips on window blur\n this._tooltipBlurHandler = () => this.hideAllTooltips();\n window.addEventListener('blur', this._tooltipBlurHandler);\n\n // Hide tooltips on escape key\n this._tooltipEscapeHandler = (e) => {\n if (e.key === 'Escape') {\n this.hideAllTooltips();\n }\n };\n document.addEventListener('keydown', this._tooltipEscapeHandler);\n }\n\n /**\n * Remove global tooltip hide listeners\n */\n removeTooltipHideListeners() {\n if (this._tooltipScrollHandler) {\n this.element.removeEventListener('scroll', this._tooltipScrollHandler);\n delete this._tooltipScrollHandler;\n }\n\n if (this._tooltipBlurHandler) {\n window.removeEventListener('blur', this._tooltipBlurHandler);\n delete this._tooltipBlurHandler;\n }\n\n if (this._tooltipEscapeHandler) {\n document.removeEventListener('keydown', this._tooltipEscapeHandler);\n delete this._tooltipEscapeHandler;\n }\n }\n\n /**\n * Force hide all visible tooltips\n */\n hideAllTooltips() {\n const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle=\"tooltip\"]');\n navLinks.forEach((link) => {\n const tooltip = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);\n if (tooltip) {\n tooltip.hide();\n }\n });\n\n // Also hide any orphaned tooltips\n const visibleTooltips = document.querySelectorAll('.tooltip.show');\n visibleTooltips.forEach(tooltip => {\n tooltip.remove();\n });\n }\n\n /**\n * Cleanup on destroy\n */\n async onBeforeDestroy() {\n // Clean up tooltips\n this.destroyTooltips();\n\n // Call parent cleanup\n await super.onBeforeDestroy();\n }\n\n /**\n * Setup responsive behavior for mobile\n */\n setupResponsiveBehavior() {\n const checkMobile = () => {\n const isMobile = window.innerWidth <= 768;\n const portalContainer = document.querySelector('.portal-container');\n\n if (portalContainer) {\n if (isMobile) {\n portalContainer.classList.add('sidebar-mobile');\n } else {\n portalContainer.classList.remove('sidebar-mobile', 'sidebar-open');\n }\n }\n };\n\n // Check on load and resize\n checkMobile();\n window.addEventListener('resize', checkMobile);\n }\n\n /**\n * Static method to create a sidebar with common configuration\n */\n static createDefault(options = {}) {\n return new Sidebar({\n theme: 'sidebar-clean',\n showToggle: true,\n autoCollapseMobile: true,\n ...options\n });\n }\n\n /**\n * Static method to create a minimal sidebar\n */\n static createMinimal(options = {}) {\n return new Sidebar({\n theme: 'sidebar-clean',\n showToggle: false,\n autoCollapseMobile: false,\n ...options\n });\n }\n\n /**\n * Set sidebar theme\n */\n setSidebarTheme(theme) {\n // Remove existing theme classes\n this.removeClass('sidebar-light sidebar-dark sidebar-clean');\n\n // Add new theme\n this.sidebarTheme = theme;\n this.addClass(theme);\n\n return this;\n }\n\n /**\n * Quick method to show/hide the sidebar\n */\n show() {\n return this.setSidebarState('normal');\n }\n\n hide() {\n return this.setSidebarState('hidden');\n }\n\n collapse() {\n return this.setSidebarState('collapsed');\n }\n\n expand() {\n return this.setSidebarState('normal');\n }\n\n /**\n * Add pulse effect to toggle button\n */\n pulseToggle() {\n const toggleButton = this.element.querySelector('.sidebar-toggle');\n if (toggleButton) {\n toggleButton.classList.add('pulse');\n\n // Remove pulse after 3 seconds or first click\n const removePulse = () => {\n toggleButton.classList.remove('pulse');\n toggleButton.removeEventListener('click', removePulse);\n };\n\n toggleButton.addEventListener('click', removePulse, { once: true });\n setTimeout(removePulse, 3000);\n }\n return this;\n }\n\n /**\n * Utility method to quickly add a simple menu item\n */\n addSimpleMenuItem(menuName, text, route, icon = 'bi-circle') {\n const menu = this.menus.get(menuName);\n if (menu) {\n menu.items = menu.items || [];\n menu.items.push({\n text: text,\n route: route,\n icon: icon\n });\n\n if (this.activeMenuName === menuName) {\n this.render();\n }\n }\n return this;\n }\n\n /**\n * Utility method to quickly create and set a simple menu\n */\n setSimpleMenu(name, header, items) {\n const menu = {\n name: name,\n header: header,\n items: items\n };\n\n this.addMenu(name, menu);\n this.setActiveMenu(name);\n\n return this;\n }\n\n}\n\nexport default Sidebar;\n","/**\n * PageHeader - Displays page title, description, and actions above page content\n * Used by PortalApp to show consistent page headers\n */\n\nimport View from '@core/View.js';\n\nclass PageHeader extends View {\n constructor(options = {}) {\n super({\n tagName: 'div',\n className: 'page-header',\n ...options\n });\n\n // Configuration\n this.style = options.style || 'default'; // 'default' | 'minimal' | 'breadcrumb'\n this.size = options.size || 'md'; // 'sm' | 'md' | 'lg' | 'xl'\n this.showIcon = options.showIcon !== false;\n this.showDescription = options.showDescription !== false;\n this.showBreadcrumbs = options.showBreadcrumbs || false;\n \n // Current page reference\n this.currentPage = null;\n }\n\n async getTemplate() {\n if (this.style === 'minimal') {\n return this.getMinimalTemplate();\n } else if (this.style === 'breadcrumb') {\n return this.getBreadcrumbTemplate();\n }\n return this.getDefaultTemplate();\n }\n\n getDefaultTemplate() {\n return `\n {{#data.hasPage}}\n <div class=\"page-header-content page-header-{{data.size}}\">\n <div class=\"page-header-main\">\n <div class=\"page-header-info\">\n {{#data.showIcon}}\n {{#data.pageIcon}}\n <div class=\"page-icon\">\n <i class=\"{{data.pageIcon}}\"></i>\n </div>\n {{/data.pageIcon}}\n {{/data.showIcon}}\n \n <div class=\"page-title-group\">\n <h1 class=\"page-title\">{{data.pageTitle}}</h1>\n {{#data.showDescription}}\n {{#data.pageDescription}}\n <p class=\"page-description text-muted\">{{data.pageDescription}}</p>\n {{/data.pageDescription}}\n {{/data.showDescription}}\n </div>\n </div>\n\n {{#data.hasActions}}\n <div class=\"page-actions\">\n {{#data.actions}}\n <button class=\"btn {{buttonClass}}\" \n data-action=\"{{action}}\"\n type=\"button\">\n {{#icon}}<i class=\"{{icon}} me-1\"></i>{{/icon}}\n {{label}}\n </button>\n {{/data.actions}}\n </div>\n {{/data.hasActions}}\n </div>\n </div>\n {{/data.hasPage}}\n `;\n }\n\n getMinimalTemplate() {\n return `\n {{#data.hasPage}}\n <div class=\"page-header-content page-header-minimal\">\n <h1 class=\"page-title\">\n {{#data.showIcon}}\n {{#data.pageIcon}}<i class=\"{{data.pageIcon}} me-2\"></i>{{/data.pageIcon}}\n {{/data.showIcon}}\n {{data.pageTitle}}\n </h1>\n </div>\n {{/data.hasPage}}\n `;\n }\n\n getBreadcrumbTemplate() {\n return `\n {{#data.hasPage}}\n <div class=\"page-header-content page-header-breadcrumb\">\n {{#data.showBreadcrumbs}}\n <nav aria-label=\"breadcrumb\">\n <ol class=\"breadcrumb mb-2\">\n {{#data.breadcrumbs}}\n <li class=\"breadcrumb-item {{#active}}active{{/active}}\">\n {{#href}}<a href=\"{{href}}\">{{label}}</a>{{/href}}\n {{^href}}{{label}}{{/href}}\n </li>\n {{/data.breadcrumbs}}\n </ol>\n </nav>\n {{/data.showBreadcrumbs}}\n \n <div class=\"d-flex justify-content-between align-items-start\">\n <h1 class=\"page-title\">\n {{#data.showIcon}}\n {{#data.pageIcon}}<i class=\"{{data.pageIcon}} me-2\"></i>{{/data.pageIcon}}\n {{/data.showIcon}}\n {{data.pageTitle}}\n </h1>\n \n {{#data.hasActions}}\n <div class=\"page-actions\">\n {{#data.actions}}\n <button class=\"btn {{buttonClass}}\" \n data-action=\"{{action}}\"\n type=\"button\">\n {{#icon}}<i class=\"{{icon}} me-1\"></i>{{/icon}}\n {{label}}\n </button>\n {{/data.actions}}\n </div>\n {{/data.hasActions}}\n </div>\n \n {{#data.showDescription}}\n {{#data.pageDescription}}\n <p class=\"page-description text-muted mt-2\">{{data.pageDescription}}</p>\n {{/data.pageDescription}}\n {{/data.showDescription}}\n </div>\n {{/data.hasPage}}\n `;\n }\n\n async onBeforeRender() {\n await super.onBeforeRender();\n\n const page = this.currentPage;\n const hasPage = !!page;\n\n // Debug logging\n if (page) {\n console.log('PageHeader page:', {\n title: page.title,\n displayName: page.displayName,\n name: page.name,\n pageName: page.pageName,\n icon: page.icon,\n pageIcon: page.pageIcon,\n pageDescription: page.pageDescription,\n description: page.description\n });\n }\n\n // Get headerActions from page options, instance, or constructor.prototype\n const headerActions = page?.options?.headerActions ||\n page?.headerActions || \n page?.constructor?.prototype?.headerActions || \n [];\n\n this.data = {\n hasPage,\n pageTitle: page?.title || page?.displayName || page?.name || page?.pageName || '',\n pageIcon: page?.icon || page?.pageIcon || '',\n pageDescription: page?.pageDescription || page?.description || '',\n showIcon: this.showIcon,\n showDescription: this.showDescription,\n showBreadcrumbs: this.showBreadcrumbs,\n breadcrumbs: page?.options?.breadcrumbs || page?.breadcrumbs || [],\n actions: headerActions,\n hasActions: headerActions.length > 0,\n size: this.size\n };\n \n console.log('PageHeader data:', this.data);\n }\n\n /**\n * Set the current page to display\n */\n async setPage(page) {\n console.log('PageHeader.setPage called with:', page?.pageName || page?.name || 'no page');\n this.currentPage = page;\n // Always render if we have a page, even if not yet mounted\n // This handles the case where setPage is called during initial app setup\n if (page) {\n console.log('PageHeader.setPage calling render()');\n await this.render();\n console.log('PageHeader.setPage render() complete');\n }\n }\n\n /**\n * Get the current page\n */\n getPage() {\n return this.currentPage;\n }\n\n /**\n * Handle action clicks from page header buttons\n */\n async onActionDefault(action, event, element) {\n // Emit to page if it has a handler\n if (this.currentPage && typeof this.currentPage.onHeaderAction === 'function') {\n await this.currentPage.onHeaderAction(action, event, element);\n return true;\n }\n\n // Emit event for parent to handle\n this.emit('action', {\n action,\n event,\n element,\n page: this.currentPage\n });\n\n return false;\n }\n}\n\nexport default PageHeader;\n","/**\n * DeniedPage - Access Denied page for MOJO Framework\n * Displays when a user attempts to access a page without proper permissions\n */\n\nimport Page from '@core/Page.js';\n\nclass DeniedPage extends Page {\n constructor(options = {}) {\n super({\n pageName: 'Access Denied',\n route: '/denied',\n title: 'Access Denied',\n pageIcon: 'bi bi-shield-x',\n template: `\n <div class=\"container mt-5\">\n <div class=\"row justify-content-center\">\n <div class=\"col-md-8 col-lg-6\">\n <div class=\"text-center mb-4\">\n <i class=\"bi bi-shield-x text-muted\" style=\"font-size: 3rem;\"></i>\n <h2 class=\"mt-3 mb-2\">Access Denied</h2>\n <p class=\"text-muted\">You don't have permission to access this page.</p>\n </div>\n\n {{#deniedPage}}\n <div class=\"card border-0 shadow-sm mb-4\">\n <div class=\"card-body\">\n <h6 class=\"card-subtitle mb-2 text-muted\">Requested Page</h6>\n <h5 class=\"card-title\">\n <i class=\"{{pageIcon}} me-2\"></i>\n {{displayName}}\n </h5>\n {{#route}}\n <p class=\"card-text text-muted small\">{{route}}</p>\n {{/route}}\n {{#description}}\n <p class=\"card-text\">{{description}}</p>\n {{/description}}\n\n {{#requiredPermissions}}\n <div class=\"mt-3\">\n <h6 class=\"mb-2\">Required Permissions:</h6>\n {{#permissions}}\n <span class=\"badge bg-light text-dark me-1 mb-1\">{{.}}</span>\n {{/permissions}}\n {{^permissions}}\n <span class=\"text-muted small\">Authentication required</span>\n {{/permissions}}\n </div>\n {{/requiredPermissions}}\n </div>\n </div>\n {{/deniedPage}}\n\n <div class=\"d-grid gap-2 d-md-flex justify-content-md-center\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"go-back\">\n <i class=\"bi bi-arrow-left me-1\"></i>\n Go Back\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"go-home\">\n <i class=\"bi bi-house me-1\"></i>\n Home\n </button>\n {{#showLogin}}\n <button type=\"button\" class=\"btn btn-outline-primary\" data-action=\"login\">\n <i class=\"bi bi-box-arrow-in-right me-1\"></i>\n Login\n </button>\n {{/showLogin}}\n </div>\n\n {{#currentUser}}\n <div class=\"text-center mt-4\">\n <small class=\"text-muted\">\n Logged in as <strong>{{username}}</strong>\n </small>\n </div>\n {{/currentUser}}\n </div>\n </div>\n </div>\n `,\n ...options\n });\n\n // Store the denied page instance\n this.deniedPage = null;\n this.deniedPageOptions = null;\n }\n\n /**\n * Handle route parameters - expect denied page info\n */\n async onParams(params = {}, query = {}) {\n await super.onParams(params, query);\n\n // If page info is passed in params\n if (params.page) {\n this.deniedPage = params.page;\n this.deniedPageOptions = params.page.options || params.page.pageOptions || {};\n } else if (query.page) {\n // Handle page name from query string\n this.deniedPageName = query.page;\n }\n }\n\n /**\n * Set the denied page instance\n */\n setDeniedPage(pageInstance) {\n this.deniedPage = pageInstance;\n this.deniedPageOptions = pageInstance?.options || pageInstance?.pageOptions || {};\n return this;\n }\n\n /**\n * Get view data for template rendering\n */\n async getViewData() {\n const app = this.getApp();\n\n // Get current user info\n const currentUser = app?.activeUser || app?.getCurrentUser?.() || null;\n\n // Process denied page info\n let deniedPageInfo = null;\n if (this.deniedPage) {\n const permissions = this.deniedPageOptions?.permissions ||\n this.deniedPage.options?.permissions ||\n this.deniedPage.pageOptions?.permissions;\n\n deniedPageInfo = {\n displayName: this.deniedPage.displayName || this.deniedPage.pageName || this.deniedPage.title || 'Unknown Page',\n pageName: this.deniedPage.pageName,\n route: this.deniedPage.route,\n description: this.deniedPage.pageDescription || this.deniedPage.description,\n pageIcon: this.deniedPage.pageIcon || 'bi bi-file-text',\n requiredPermissions: permissions ? {\n permissions: Array.isArray(permissions) ? permissions : [permissions]\n } : null\n };\n } else if (this.deniedPageName) {\n deniedPageInfo = {\n displayName: this.deniedPageName,\n pageName: this.deniedPageName,\n pageIcon: 'bi bi-file-text'\n };\n }\n\n return {\n deniedPage: deniedPageInfo,\n currentUser: currentUser ? {\n username: currentUser.username || currentUser.name || currentUser.email || 'Unknown User',\n name: currentUser.name,\n email: currentUser.email\n } : null,\n showLogin: !currentUser // Show login button if not authenticated\n };\n }\n\n /**\n * Handle going back to previous page\n */\n async handleActionGoBack(event, element) {\n event.preventDefault();\n\n // Try to go back in browser history\n if (window.history.length > 1) {\n window.history.back();\n } else {\n // Fallback to home page\n await this.handleActionGoHome(event, element);\n }\n }\n\n /**\n * Handle navigation to home page\n */\n async handleActionGoHome(event, element) {\n event.preventDefault();\n\n const app = this.getApp();\n if (app) {\n await app.navigateToDefault();\n } else {\n // Fallback navigation\n window.location.href = '/';\n }\n }\n\n /**\n * Handle login action\n */\n async handleActionLogin(event, element) {\n event.preventDefault();\n\n const app = this.getApp();\n\n // Try to navigate to login page\n if (app) {\n try {\n await app.showPage('login');\n } catch (error) {\n // If login page doesn't exist, try auth route\n try {\n await app.navigate('/login');\n } catch (navError) {\n // Fallback - emit login required event\n this.emit('login-required', {\n returnUrl: this.deniedPage?.route || window.location.pathname\n });\n\n // Show message if no handlers\n setTimeout(() => {\n app?.showInfo?.('Please contact your administrator for access.');\n }, 100);\n }\n }\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n // Set appropriate page title\n const pageName = this.deniedPage?.pageName || this.deniedPageName;\n if (pageName) {\n this.setMeta({\n title: `Access Denied - ${pageName}`\n });\n }\n\n // Log access denial for security monitoring\n console.warn('Access denied to page:', {\n page: this.deniedPage?.pageName || this.deniedPageName,\n route: this.deniedPage?.route,\n permissions: this.deniedPageOptions?.permissions,\n timestamp: new Date().toISOString()\n });\n }\n\n /**\n * Static helper to show access denied for a specific page\n */\n static showForPage(app, pageInstance) {\n const deniedPage = new DeniedPage();\n deniedPage.setDeniedPage(pageInstance);\n return app.showPage(deniedPage);\n }\n}\n\nexport default DeniedPage;\n","/**\n * NotFoundPage - 404 Not Found page for MOJO Framework\n * Displays when a user attempts to access a non-existent page or route\n */\n\nimport Page from '@core/Page.js';\n\nclass NotFoundPage extends Page {\n constructor(options = {}) {\n super({\n pageName: '404',\n route: '/404',\n title: '404 - Page Not Found',\n pageIcon: 'bi bi-search',\n template: `\n <div class=\"container mt-5\">\n <div class=\"row justify-content-center\">\n <div class=\"col-md-8 col-lg-6\">\n <div class=\"text-center mb-4\">\n <i class=\"bi bi-search text-muted\" style=\"font-size: 3rem;\"></i>\n <h2 class=\"mt-3 mb-2\">Page Not Found</h2>\n <p class=\"text-muted\">The page you're looking for doesn't exist.</p>\n </div>\n\n {{#path}}\n <div class=\"card border-0 shadow-sm mb-4\">\n <div class=\"card-body text-center\">\n <h6 class=\"card-subtitle mb-2 text-muted\">Requested Path</h6>\n <code class=\"text-primary\">{{path}}</code>\n </div>\n </div>\n {{/path}}\n\n <div class=\"d-grid gap-2 d-md-flex justify-content-md-center\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"go-back\">\n <i class=\"bi bi-arrow-left me-1\"></i>\n Go Back\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"go-home\">\n <i class=\"bi bi-house me-1\"></i>\n Home\n </button>\n </div>\n </div>\n </div>\n </div>\n `,\n ...options\n });\n\n // Store the not found path\n this.path = null;\n }\n\n /**\n * Handle route parameters\n */\n async onParams(params = {}, query = {}) {\n await super.onParams(params, query);\n\n // Store path from params or query\n if (params.path) {\n this.path = params.path;\n }\n if (query.path) {\n this.path = query.path;\n }\n }\n\n /**\n * Set not found path\n */\n setInfo(path) {\n this.path = path || null;\n return this;\n }\n\n /**\n * Handle going back to previous page\n */\n async handleActionGoBack(event, _element) {\n event.preventDefault();\n\n // Try to go back in browser history\n if (window.history.length > 1) {\n window.history.back();\n } else {\n // Fallback to home page\n await this.handleActionGoHome(event, _element);\n }\n }\n\n /**\n * Handle navigation to home page\n */\n async handleActionGoHome(event, _element) {\n event.preventDefault();\n\n const app = this.getApp();\n if (app) {\n await app.navigateToDefault();\n } else {\n // Fallback navigation\n window.location.href = '/';\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n // Set appropriate page title\n if (this.path) {\n this.setMeta({\n title: `404 - ${this.path} Not Found`\n });\n }\n\n // Log 404 for analytics\n console.warn('404 Not Found:', {\n path: this.path,\n timestamp: new Date().toISOString()\n });\n }\n\n /**\n * Static helper to show 404 for a specific path\n */\n static showForPath(app, path) {\n const notFoundPage = new NotFoundPage();\n notFoundPage.setInfo(path);\n return notFoundPage.render(); // Just render, don't navigate\n }\n}\n\nexport default NotFoundPage;\n","/**\n * PortalApp - Complete portal application extending WebApp\n * Provides built-in navigation, sidebar, and content management\n * Clean, simple implementation that reuses WebApp and View logic\n */\n\nimport WebApp from '@core/WebApp.js';\nimport TopNav from '@core/views/navigation/TopNav.js';\nimport Sidebar from '@core/views/navigation/Sidebar.js';\nimport PageHeader from '@core/views/navigation/PageHeader.js';\nimport DeniedPage from '@core/pages/DeniedPage.js';\nimport TokenManager from '@core/services/TokenManager.js';\nimport {User} from '@core/models/User.js';\nimport {Group } from '@core/models/Group.js';\nimport {Member} from '@core/models/Member.js';\nimport NotFoundPage from '@core/pages/NotFoundPage.js';\nimport ToastService from '@core/services/ToastService.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\nexport default class PortalApp extends WebApp {\n constructor(config = {}) {\n // Pass core WebApp config through\n super(config);\n\n this.sidebarConfig = config.sidebar;\n // Portal-specific configuration (clean flat structure)\n // if (config.sidebar && config.sidebar.menus) {\n // this.sidebarConfig.menus = config.sidebar.menus;\n // this.sidebarConfig.groupSelectorMode = config.sidebar.groupSelectorMode || \"inline\";\n // if (config.sidebar.groupHeader) {\n // this.sidebarConfig.groupHeader = config.sidebar.groupHeader;\n // }\n // } else if (config.sidebar.menu) {\n // this.sidebarConfig.menu = config.sidebar.menu;\n // } else if (config.sidebar.items) {\n // this.sidebarConfig.menu = config.sidebar;\n // }\n\n this.topbarConfig = config.topbar || {};\n\n // Legacy support - topnav -> topbar\n if (config.topnav && !config.topbar) {\n this.topbarConfig = config.topnav;\n }\n\n // Page header configuration\n this.showPageHeader = config.showPageHeader || false;\n this.pageHeaderConfig = config.pageHeader || {};\n\n // Portal components\n this.sidebar = null;\n this.topbar = null;\n this.topnav = null; // Legacy reference\n this.pageHeader = null;\n this.tokenManager = new TokenManager();\n\n // Active group management\n this.activeGroup = null;\n // Portal state - Load from localStorage first, then fallback to config\n if (!this.isMobile()) {\n this.sidebarCollapsed = this.loadSidebarState() ??\n (this.sidebarConfig.defaultCollapsed || false);\n } else {\n this.sidebarCollapsed = this.sidebarConfig.defaultCollapsed || false;\n }\n this.setupPageContainer();\n\n this.toast = new ToastService();\n this.Dialog = Dialog;\n\n this.registerPage(\"denied\", DeniedPage);\n this.registerPage(\"404\", NotFoundPage);\n }\n\n /**\n * Override WebApp start to setup portal layout\n */\n async start() {\n // Call parent start (handles router, error handling, etc.)\n // Setup router\n await this.checkAuthStatus();\n\n this.events.on('auth:unauthorized', () => {\n this.tokenManager.clearTokens();\n this.rest.clearAuth();\n this.setActiveUser(null);\n return;\n });\n\n this.events.on('auth:logout', () => {\n this.tokenManager.clearTokens();\n this.rest.clearAuth();\n this.setActiveUser(null);\n return;\n });\n\n this.events.on(\"browser:focus\", () => {\n if (!this.activeUser) return;\n this.tokenManager.checkAndRefreshTokens(this);\n });\n\n this.events.on('portal:action', this.onPortalAction.bind(this));\n\n if (this.activeUser) {\n // Check and load active group after auth\n await this.checkActiveGroup();\n }\n\n await this.setupRouter();\n\n // Mark as started\n this.isStarted = true;\n\n // Emit app ready event\n this.events.emit('app:ready', { app: this });\n\n // Prompt passkey setup after login if not dismissed\n if (this.activeUser && !this.activeUser.get(\"has_passkey\")) {\n this.showPasskeySetup();\n }\n\n\n }\n\n async checkAuthStatus() {\n const tokenStatus = this.tokenManager.checkTokenStatus();\n\n // Handle logout scenarios\n if (tokenStatus.action === 'logout') {\n this.events.emit('auth:unauthorized', { app: this });\n return false;\n }\n\n // Handle refresh scenarios - attempt refresh if needed\n if (tokenStatus.action === 'refresh') {\n const refreshed = await this.tokenManager.checkAndRefreshTokens(this);\n if (!refreshed) {\n // If refresh failed, checkAndRefreshTokens already handled logout\n return false;\n }\n }\n\n // At this point we have a valid token\n const token = this.tokenManager.getTokenInstance();\n\n // If user already loaded, just start auto-refresh and return\n if (this.activeUser) {\n this.tokenManager.startAutoRefresh(this);\n return true;\n }\n\n // Load user data\n this.rest.setAuthToken(token.token);\n const user = new User({ id: token.getUserId() });\n const resp = await user.fetch();\n if (!resp.success) {\n this.tokenManager.clearTokens();\n this.events.emit('auth:unauthorized', { app: this, error: resp.error });\n return false;\n }\n\n this.setActiveUser(user);\n this.tokenManager.startAutoRefresh(this);\n return true;\n }\n\n /**\n * Check and load active group from storage\n */\n async checkActiveGroup() {\n // First check URL search params for group parameter\n const urlParams = new URLSearchParams(window.location.search);\n const urlGroupId = urlParams.get('group');\n\n // Determine which group ID to use: URL param takes priority\n const groupId = urlGroupId || this.loadActiveGroupId();\n\n if (groupId) {\n try {\n const group = new Group({ id: groupId });\n const resp = await group.fetch();\n if (!resp.success || !resp.data.status) {\n this.clearActiveGroup();\n console.warn('Failed to load active group:', resp.statusText);\n return;\n }\n\n this.activeGroup = group;\n // If we got the group from URL, save it as the new active group\n if (urlGroupId) {\n this.saveActiveGroupId(groupId);\n }\n\n if (this.activeUser) {\n this.activeUser.member = new Member();\n await this.activeUser.member.fetchForGroup(group.id);\n }\n\n // Emit event that group was loaded\n this.events.emit('group:loaded', { group: this.activeGroup });\n\n\n } catch (error) {\n console.warn('Failed to load active group:', error);\n // If URL group failed, try to clear it and fall back to stored group\n if (urlGroupId && !this.loadActiveGroupId()) {\n // URL group failed and no stored group, clear everything\n this.clearActiveGroupId();\n } else if (urlGroupId) {\n // URL group failed but we have a stored group, try that instead\n const storedGroupId = this.loadActiveGroupId();\n if (storedGroupId && storedGroupId !== urlGroupId) {\n try {\n const fallbackGroup = new Group({ id: storedGroupId });\n await fallbackGroup.fetch();\n this.activeGroup = fallbackGroup;\n this.events.emit('group:loaded', { group: this.activeGroup });\n\n } catch (fallbackError) {\n console.warn('Fallback to stored group also failed:', fallbackError);\n this.clearActiveGroupId();\n }\n }\n }\n }\n }\n }\n\n\n /**\n * Set the active group\n */\n async setActiveGroup(group) {\n const previousGroup = this.activeGroup;\n this.activeGroup = group;\n\n // Save to storage\n if (group && group.get('id')) {\n this.saveActiveGroupId(group.get('id'));\n } else {\n this.clearActiveGroupId();\n }\n\n if (this.activeUser) {\n this.activeUser.member = new Member();\n await this.activeUser.member.fetchForGroup(group.id);\n }\n\n // Emit event\n this.events.emit('group:changed', {\n group,\n previousGroup,\n app: this\n });\n\n const page = this.getCurrentPage();\n if (page) {\n page.onGroupChange(group);\n }\n\n this.router.updateUrl({group:group.id}, { replace: true });\n\n return this;\n }\n\n /**\n * Get the active group\n */\n getActiveGroup() {\n return this.activeGroup;\n }\n\n /**\n * Clear the active group\n */\n async clearActiveGroup() {\n const previousGroup = this.activeGroup;\n this.activeGroup = null;\n this.clearActiveGroupId();\n // Emit event\n this.events.emit('group:cleared', {\n previousGroup,\n app: this\n });\n return this;\n }\n\n /**\n * Save active group ID to localStorage\n */\n saveActiveGroupId(groupId) {\n try {\n const key = this.getActiveGroupStorageKey();\n localStorage.setItem(key, groupId.toString());\n } catch (error) {\n console.warn('Failed to save active group ID:', error);\n }\n }\n\n /**\n * Load active group ID from localStorage\n */\n loadActiveGroupId() {\n try {\n const key = this.getActiveGroupStorageKey();\n return localStorage.getItem(key);\n } catch (error) {\n console.warn('Failed to load active group ID:', error);\n return null;\n }\n }\n\n /**\n * Clear active group ID from localStorage\n */\n clearActiveGroupId() {\n try {\n const key = this.getActiveGroupStorageKey();\n localStorage.removeItem(key);\n } catch (error) {\n console.warn('Failed to clear active group ID:', error);\n }\n }\n\n /**\n * Get storage key for active group ID\n */\n getActiveGroupStorageKey() {\n return `active_group_id`;\n }\n\n /**\n * Set portal profile to localStorage\n */\n setPortalProfile(profile) {\n try {\n localStorage.setItem('portal_profile', profile);\n } catch (error) {\n console.warn('Failed to save portal profile:', error);\n }\n }\n\n /**\n * Check if user needs to select a group\n */\n needsGroupSelection() {\n return !this.activeGroup;\n }\n\n /**\n * Setup layout based on configuration\n */\n setupPageContainer() {\n const container = typeof this.container === 'string'\n ? document.querySelector(this.container)\n : this.container;\n\n if (!container) {\n throw new Error(`Portal container not found: ${this.container}`);\n }\n\n // Create clean portal layout\n const showSidebar = this.sidebarConfig && Object.keys(this.sidebarConfig).length > 0;\n const showTopbar = this.topbarConfig && Object.keys(this.topbarConfig).length > 0;\n\n // If page header is enabled, wrap content in two containers\n const contentMarkup = this.showPageHeader ? `\n <div class=\"portal-content\" id=\"portal-content\">\n <div id=\"page-header\"></div>\n <div id=\"page-container\">\n <!-- Pages render here -->\n </div>\n </div>\n ` : `\n <div class=\"portal-content\" id=\"page-container\">\n <!-- Pages render here -->\n </div>\n `;\n\n container.innerHTML = `\n <div class=\"portal-layout hide-sidebar\">\n ${showSidebar ? '<div id=\"portal-sidebar\"></div>' : ''}\n <div class=\"portal-body\">\n ${showTopbar ? '<div id=\"portal-topnav\"></div>' : ''}\n ${contentMarkup}\n </div>\n </div>\n `;\n\n // Set page container for WebApp\n this.pageContainer = '#page-container';\n\n // Add portal CSS classes and apply saved state\n container.classList.add('portal-container');\n\n // Setup page container\n this.setupPortalComponents();\n\n // Apply the saved sidebar state\n this.applySidebarState(container);\n }\n\n /**\n * Setup portal components\n */\n async setupPortalComponents() {\n await this.setupSidebar();\n await this.setupTopbar();\n await this.setupPageHeader();\n this.setupPortalEvents();\n }\n\n /**\n * Setup sidebar component\n */\n async setupSidebar() {\n if (!this.sidebarConfig || Object.keys(this.sidebarConfig).length === 0) return;\n\n this.sidebar = new Sidebar({\n containerId: 'portal-sidebar',\n ...this.sidebarConfig\n });\n\n await this.sidebar.render();\n }\n\n /**\n * Setup topbar component\n */\n async setupTopbar() {\n if (!this.topbarConfig || Object.keys(this.topbarConfig).length === 0) return;\n\n // Map config to TopNav format\n this.topbar = new TopNav({\n containerId: \"portal-topnav\",\n brandText: this.topbarConfig.brand || this.brand || this.title,\n brandRoute: this.topbarConfig.brandRoute || '/',\n brandIcon: this.topbarConfig.brandIcon || this.brandIcon,\n navItems: this.topbarConfig.leftItems || [],\n rightItems: this.topbarConfig.rightItems || [],\n displayMode: this.topbarConfig.displayMode || 'both',\n showSidebarToggle: this.topbarConfig.showSidebarToggle || false,\n ...this.topbarConfig\n });\n\n await this.topbar.render();\n\n // Legacy support\n this.topnav = this.topbar;\n }\n\n /**\n * Setup page header component\n */\n async setupPageHeader() {\n if (!this.showPageHeader) return;\n\n this.pageHeader = new PageHeader({\n containerId: 'page-header',\n style: this.pageHeaderConfig.style || 'default',\n showIcon: this.pageHeaderConfig.showIcon !== false,\n showDescription: this.pageHeaderConfig.showDescription !== false,\n showBreadcrumbs: this.pageHeaderConfig.showBreadcrumbs || false,\n ...this.pageHeaderConfig\n });\n\n // Render into the portal-content container\n const headerContainer = document.getElementById('page-header');\n if (headerContainer) {\n await this.pageHeader.render(true, headerContainer);\n }\n }\n\n /**\n * Setup portal event handling\n */\n setupPortalEvents() {\n // Handle sidebar toggle via event delegation\n document.addEventListener('click', (event) => {\n if (event.target.closest('[data-action=\"toggle-sidebar\"]')) {\n event.preventDefault();\n this.toggleSidebar();\n }\n });\n\n // Handle responsive changes\n if (window.ResizeObserver) {\n const resizeObserver = new ResizeObserver(() => {\n this.handleResponsive();\n });\n resizeObserver.observe(document.body);\n this._resizeObserver = resizeObserver;\n } else {\n // Fallback for older browsers\n this._resizeHandler = () => this.handleResponsive();\n window.addEventListener('resize', this._resizeHandler);\n }\n\n // Initial responsive setup\n this.handleResponsive();\n }\n\n /**\n * Toggle sidebar state\n */\n toggleSidebar() {\n if (!this.sidebar) return;\n\n const container = document.querySelector('.portal-container');\n const isMobile = this.isMobile();\n\n if (isMobile) {\n container.classList.toggle('hide-sidebar');\n } else {\n container.classList.toggle('collapse-sidebar');\n this.sidebarCollapsed = !this.sidebarCollapsed;\n\n // Save the new state\n this.saveSidebarState(this.sidebarCollapsed);\n }\n\n this.events.emit('sidebar:toggled', {\n collapsed: this.sidebarCollapsed,\n mobile: isMobile\n });\n }\n\n /**\n * Handle responsive layout\n */\n handleResponsive() {\n const container = document.querySelector('.portal-container');\n if (!container) return;\n const isMobile = this.isMobile();\n\n if (isMobile) {\n container.classList.add('mobile-layout');\n if (!container.classList.contains('hide-sidebar')) {\n container.classList.add('hide-sidebar');\n }\n } else {\n container.classList.remove('mobile-layout', 'hide-sidebar');\n }\n\n this.events.emit('responsive:changed', { mobile: isMobile });\n }\n\n getPortalContainer() {\n return document.querySelector('.portal-container');\n }\n\n isMobile() {\n return window.innerWidth < 768;\n }\n\n hasMobileLayout() {\n return this.getPortalContainer().classList.contains('mobile-layout');\n }\n\n /**\n * Override showPage to update navigation\n */\n async showPage(page, query = {}, params = {}, options = {}) {\n const result = await super.showPage(page, query, params, options);\n\n if (this.hasMobileLayout()) {\n this.getPortalContainer().classList.add('hide-sidebar');\n }\n\n if (this.currentPage) {\n this.updateNavigation(this.currentPage);\n }\n\n return result;\n }\n\n /**\n * Update navigation active states\n */\n updateNavigation(page) {\n // Update sidebar active state\n if (this.sidebar && this.sidebar.setActivePage) {\n this.sidebar.setActivePage(page.route);\n }\n\n // Update topbar active state\n if (this.topbar && this.topbar.setActivePage) {\n this.topbar.setActivePage(page.route);\n }\n\n // Update page header\n if (this.pageHeader) {\n this.pageHeader.setPage(page);\n }\n\n this.events.emit('portal:page-changed', { page });\n }\n\n /**\n * Set active user\n */\n setActiveUser(user) {\n this.activeUser = user;\n\n if (this.topbar) {\n this.topbar.setUser(user);\n }\n\n\n this.events.emit('portal:user-changed', { user });\n }\n\n /**\n * Get the active user (for backward compatibility)\n */\n getActiveUser() {\n return this.activeUser;\n }\n\n /**\n * Save sidebar state to localStorage\n */\n saveSidebarState(collapsed) {\n try {\n const key = this.getSidebarStorageKey();\n localStorage.setItem(key, JSON.stringify(collapsed));\n } catch (error) {\n console.warn('Failed to save sidebar state:', error);\n }\n }\n\n /**\n * Load sidebar state from localStorage\n */\n loadSidebarState() {\n try {\n const key = this.getSidebarStorageKey();\n const saved = localStorage.getItem(key);\n return saved !== null ? JSON.parse(saved) : null;\n } catch (error) {\n console.warn('Failed to load sidebar state:', error);\n return null;\n }\n }\n\n /**\n * Get storage key for sidebar state (allows multiple apps on same domain)\n */\n getSidebarStorageKey() {\n // Use app title/name to create unique key\n const appKey = this.title ? this.title.replace(/\\s+/g, '_').toLowerCase() : 'portal_app';\n return `${appKey}_sidebar_collapsed`;\n }\n\n /**\n * Apply saved sidebar state to the UI\n */\n applySidebarState(container = null) {\n if (!container) {\n container = document.querySelector('.portal-container');\n }\n\n if (!container) return;\n\n if (this.sidebarCollapsed) {\n container.classList.add('collapse-sidebar');\n } else {\n container.classList.remove('collapse-sidebar');\n }\n }\n\n /**\n * Clear saved sidebar state\n */\n clearSidebarState() {\n try {\n const key = this.getSidebarStorageKey();\n localStorage.removeItem(key);\n } catch (error) {\n console.warn('Failed to clear sidebar state:', error);\n }\n }\n\n async changePassword() {\n const data = await this.showForm({\n title: \"Change Password\",\n fields: [\n {\n name: 'current_password', type: 'password',\n label: 'Current Password', required: true,\n showToggle: true, // default, can omit\n strengthMeter: true,\n capsLockWarning: true,\n\n },\n {\n name: 'new_password', type: 'password', label: 'New Password', required: true,\n showToggle: true,\n passwordUsage: 'new', // sets autocomplete to 'new-password'\n\n strengthMeter: true,\n capsLockWarning: true,\n attributes: {\n // optional, override autocomplete if needed\n autocomplete: 'new-password'\n }\n },\n {\n name: 'confirm_password', type: 'password', label: 'Confirm Password', required: true,\n showToggle: true,\n passwordUsage: 'new', // sets autocomplete to 'new-password'\n\n strengthMeter: true,\n capsLockWarning: true,\n attributes: {\n // optional, override autocomplete if needed\n // autocomplete: 'new-password'\n }\n }\n ],\n submitLabel: 'Change Password'\n });\n if (data) {\n if (data.new_password === data.confirm_password) {\n // Perform password change logic here\n const resp = await this.activeUser.save(data);\n if (resp.status === 200) {\n this.toast.success('Password changed successfully');\n } else {\n this.toast.error('Failed to change password');\n }\n } else {\n this.toast.error('Passwords do not match');\n }\n }\n }\n\n onPortalAction(action) {\n switch (action.action) {\n case 'logout':\n this.tokenManager.clearTokens();\n this.rest.clearAuth();\n this.setActiveUser(null);\n break;\n case 'profile':\n this.showProfile();\n break;\n case 'change-password':\n this.changePassword();\n break;\n default:\n console.warn(`Unknown portal action: ${action}`);\n }\n }\n\n async showProfile() {\n if (!this.activeUser) {\n this.showError(\"No user is currently logged in\");\n return;\n }\n\n try {\n const { UserProfileView } = await import('@core/views/user/index.js');\n const profileView = new UserProfileView({ model: this.activeUser });\n\n await Dialog.showDialog({\n body: profileView,\n header: null,\n size: 'lg',\n noBodyPadding: true,\n centered: false\n });\n } catch (error) {\n console.error('Error showing profile:', error);\n this.showError('Failed to load profile');\n }\n }\n\n async showPasskeySetup() {\n if (localStorage.getItem('passkey_setup_dismissed')) return;\n\n try {\n const { PasskeySetupView } = await import('@core/views/user/index.js');\n const setupView = new PasskeySetupView();\n\n setupView.on('dismiss', () => {\n // Close the dialog by finding and clicking dismiss\n const dialog = setupView.element?.closest('.modal');\n if (dialog) {\n const bsModal = bootstrap.Modal.getInstance(dialog);\n if (bsModal) bsModal.hide();\n }\n });\n\n await Dialog.showDialog({\n header: null,\n body: setupView,\n size: 'sm',\n centered: true,\n buttons: []\n });\n } catch (error) {\n console.error('Error showing passkey setup:', error);\n }\n }\n\n /**\n * Clean up portal resources\n */\n async destroy() {\n // Clean up event listeners\n // Clear active group\n this.activeGroup = null;\n\n // Clean up portal resources\n if (this._resizeObserver) {\n this._resizeObserver.disconnect();\n }\n if (this._resizeHandler) {\n window.removeEventListener('resize', this._resizeHandler);\n }\n\n // Destroy components using View lifecycle\n if (this.topbar) {\n await this.topbar.destroy();\n this.topbar = null;\n this.topnav = null;\n }\n\n if (this.sidebar) {\n await this.sidebar.destroy();\n this.sidebar = null;\n }\n\n // Call parent destroy\n await super.destroy();\n }\n\n /**\n * Static factory method\n */\n static create(config = {}) {\n return new PortalApp(config);\n }\n}\n","import FormView from './FormView.js';\nimport Page from '@core/Page.js';\n\nexport default class FormPage extends Page {\n constructor(options = {}) {\n super({\n title: 'Form Page',\n description: 'A page for submitting forms',\n icon: 'form',\n fields: [],\n template: '<div data-container=\"form-view-container\"></div>',\n className: \"form-page container-sm\",\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n await this.recreateFormView();\n }\n\n async onEnter() {\n await super.onEnter();\n if (this.formView) {\n // Recreate formView to ensure clean slate with new model\n await this.recreateFormView();\n }\n }\n\n async onGroupChange(group) {\n if (this.formView) {\n // Recreate formView to ensure clean slate with new model\n await this.recreateFormView();\n }\n }\n\n async getModel() {\n if (this.model) {\n return this.model;\n } else if (this.getApp().activeGroup) {\n return this.getApp().activeGroup;\n }\n return null;\n }\n\n async recreateFormView() {\n // Destroy old formView\n if (this.formView) {\n await this.formView.destroy();\n this.removeChild(this.formView);\n }\n\n // Create new formView with current model\n this.formView = new FormView({\n containerId: 'form-view-container',\n fields: this.options.fields,\n autosaveModelField: true\n });\n this.addChild(this.formView);\n\n const model = await this.getModel();\n if (model) {\n this.formView.setModel(model);\n }\n }\n}\n","/**\n * MustacheFormatter - Mustache wrapper for MOJO Framework\n * \n * This is now a thin wrapper around Mustache.js since pipe formatting\n * is handled directly in Model.get() and View.get() via MOJOUtils.\n * \n * The wrapper is maintained for backward compatibility and to provide\n * a consistent API for template rendering throughout the framework.\n */\n\nimport mustache from './mustache.js';\nimport dataFormatter from './DataFormatter.js';\n\nclass MustacheFormatter {\n constructor() {\n this.formatter = dataFormatter;\n this.compiledTemplates = new Map();\n }\n\n /**\n * Render template with data\n * Pipes are now handled by Model.get() and View.get() automatically\n * \n * @param {string} template - Mustache template\n * @param {object} data - Data to render (View, Model, or plain object)\n * @param {object} partials - Mustache partials\n * @returns {string} Rendered template\n */\n render(template, data, partials = {}) {\n // Simply pass through to Mustache\n // If data has a get() method, Mustache will use it automatically\n return mustache.render(template, data, partials);\n }\n\n /**\n * Compile template for reuse\n * @param {string} template - Template to compile\n * @returns {object} Compiled template tokens\n */\n compile(template) {\n const compiled = mustache.parse(template);\n this.compiledTemplates.set(template, compiled);\n return compiled;\n }\n\n /**\n * Render with compiled template\n * @param {object} compiled - Compiled template tokens\n * @param {object} data - Data to render\n * @param {object} partials - Mustache partials\n * @returns {string} Rendered template\n */\n renderCompiled(compiled, data, partials = {}) {\n return mustache.render(compiled, data, partials);\n }\n\n /**\n * Clear compiled template cache\n */\n clearCache() {\n this.compiledTemplates.clear();\n mustache.clearCache();\n }\n\n /**\n * Process and cache a template\n * @param {string} key - Cache key\n * @param {string} template - Template to cache\n * @returns {object} Cached template info\n */\n cache(key, template) {\n const compiled = this.compile(template);\n return { key, template, compiled };\n }\n\n /**\n * Get cached template\n * @param {string} key - Cache key\n * @returns {object|null} Cached template info or null\n */\n getCached(key) {\n for (const [template, compiled] of this.compiledTemplates) {\n if (template === key || compiled === key) {\n return { key, template, compiled };\n }\n }\n return null;\n }\n\n /**\n * Register a custom formatter with DataFormatter\n * @param {string} name - Formatter name\n * @param {function} formatter - Formatter function\n * @returns {MustacheFormatter} This instance for chaining\n */\n registerFormatter(name, formatter) {\n this.formatter.register(name, formatter);\n return this;\n }\n\n /**\n * Check if a string contains pipe syntax\n * @param {string} template - Template string to check\n * @returns {boolean} True if contains pipes\n */\n hasPipes(template) {\n return /\\{\\{[{]?[^}|]+\\|[^}]+\\}[}]?\\}/.test(template);\n }\n\n /**\n * Pre-process data with pipe formatters\n * This is now handled automatically by get() methods, but kept for backward compatibility\n * \n * @param {object} data - Data object\n * @param {object} pipes - Object mapping keys to pipe strings\n * @returns {object} Processed data\n */\n processData(data, pipes) {\n const processed = { ...data };\n \n for (const [key, pipeString] of Object.entries(pipes)) {\n // If data has a get method, use it (which will handle pipes)\n if (data && typeof data.get === 'function') {\n processed[key] = data.get(`${key}|${pipeString}`);\n } else {\n // For plain objects, apply formatter directly\n const value = this.getValueFromPath(data, key);\n processed[key] = this.formatter.pipe(value, pipeString);\n }\n }\n \n return processed;\n }\n\n /**\n * Get value from object using dot notation path\n * Kept for backward compatibility, but MOJOUtils.getContextData is preferred\n * \n * @param {object} obj - Source object\n * @param {string} path - Dot notation path\n * @returns {*} Value at path\n */\n getValueFromPath(obj, path) {\n if (!obj || !path) return undefined;\n \n // If obj has a get method, use it\n if (obj && typeof obj.get === 'function') {\n return obj.get(path);\n }\n \n // Otherwise use standard property navigation\n const keys = path.split('.');\n let current = obj;\n \n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n \n // Handle array index\n if (!isNaN(key) && Array.isArray(current)) {\n current = current[parseInt(key)];\n } else {\n current = current[key];\n }\n }\n \n return current;\n }\n\n /**\n * Process template to handle pipe formatters\n * @deprecated Pipes are now handled by get() methods automatically\n * @param {string} template - Original template\n * @param {object} data - Original data\n * @returns {object} {template: processedTemplate, data: processedData}\n */\n processTemplate(template, data) {\n // For backward compatibility, just return as-is\n // Pipes will be handled by get() methods during Mustache rendering\n return { template, data };\n }\n}\n\n// Create singleton instance\nconst mustacheFormatter = new MustacheFormatter();\n\n// Export both class and instance\nexport { MustacheFormatter };\nexport default mustacheFormatter;","/**\n * ProfileOverviewSection - Profile overview tab\n *\n * Shows avatar, contact, personal info, account status, and permissions peek.\n * Editable: display name, timezone, avatar.\n * Verification actions for email/phone.\n *\n * Full user graph fields used:\n * id, username, email, phone_number, display_name, first_name, last_name,\n * avatar, org (int), permissions, is_active, is_superuser, is_staff,\n * is_email_verified, is_phone_verified, requires_mfa, timezone,\n * last_login, last_activity, date_joined\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\nimport { User } from '@core/models/User.js';\n\nexport default class ProfileOverviewSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-overview-section',\n template: `\n <style>\n .po-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .po-section-label:first-child { margin-top: 0; }\n .po-field-row { display: flex; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #f0f0f0; }\n .po-field-row:last-child { border-bottom: none; }\n .po-field-label { width: 130px; font-size: 0.8rem; color: #6c757d; flex-shrink: 0; }\n .po-field-value { flex: 1; font-size: 0.88rem; color: #212529; display: flex; align-items: center; gap: 0.4rem; }\n .po-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .po-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n .po-badge-warn { font-size: 0.65rem; padding: 0.15em 0.45em; background: #fff3cd; color: #856404; border-radius: 3px; }\n .po-badge-ok { font-size: 0.65rem; padding: 0.15em 0.45em; background: #d1e7dd; color: #0f5132; border-radius: 3px; }\n .po-badge-muted { font-size: 0.65rem; padding: 0.15em 0.45em; background: #f0f0f0; color: #6c757d; border-radius: 3px; }\n .po-not-set { color: #adb5bd; font-style: italic; font-size: 0.85rem; }\n .po-perm-pill { display: inline-block; font-size: 0.72rem; padding: 0.2em 0.55em; background: #e7f1ff; color: #0d6efd; border-radius: 3px; margin: 0.1rem; }\n .po-perm-more { font-size: 0.72rem; color: #6c757d; cursor: pointer; }\n .po-perm-more:hover { color: #0d6efd; text-decoration: underline; }\n </style>\n\n <!-- Contact -->\n <div class=\"po-section-label\">Contact</div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Email</div>\n <div class=\"po-field-value\">\n {{model.email}}\n {{#model.is_email_verified|bool}}\n <span class=\"po-badge-ok\">Verified</span>\n {{/model.is_email_verified|bool}}\n {{^model.is_email_verified|bool}}\n <span class=\"po-badge-warn\">Unverified</span>\n {{/model.is_email_verified|bool}}\n </div>\n {{^model.is_email_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"verify-email\" title=\"Send verification email\"><i class=\"bi bi-envelope-check\"></i></button>\n {{/model.is_email_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"update-email\" title=\"Change email\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Phone</div>\n <div class=\"po-field-value\">\n {{#hasPhone|bool}}\n {{model.phone_number}}\n {{#model.is_phone_verified|bool}}\n <span class=\"po-badge-ok\">Verified</span>\n {{/model.is_phone_verified|bool}}\n {{^model.is_phone_verified|bool}}\n <span class=\"po-badge-warn\">Unverified</span>\n {{/model.is_phone_verified|bool}}\n {{/hasPhone|bool}}\n {{^hasPhone|bool}}\n <span class=\"po-not-set\">Not set</span>\n {{/hasPhone|bool}}\n </div>\n {{^hasPhone|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"add-phone\" title=\"Add phone number\"><i class=\"bi bi-plus\"></i></button>\n {{/hasPhone|bool}}\n {{#hasPhone|bool}}\n {{^model.is_phone_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"verify-phone\" title=\"Send verification\"><i class=\"bi bi-phone-vibrate\"></i></button>\n {{/model.is_phone_verified|bool}}\n <button type=\"button\" class=\"po-field-action\" data-action=\"update-phone\" title=\"Change phone number\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"po-field-action\" data-action=\"remove-phone\" title=\"Remove phone number\"><i class=\"bi bi-x-lg\"></i></button>\n {{/hasPhone|bool}}\n </div>\n\n <!-- Account -->\n <div class=\"po-section-label\">Account</div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Username</div>\n <div class=\"po-field-value\">{{model.username}}</div>\n <button type=\"button\" class=\"po-field-action\" data-action=\"edit-username\" title=\"Change username\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Status</div>\n <div class=\"po-field-value\">\n {{#model.is_active|bool}}<span class=\"po-badge-ok\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"po-badge-warn\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Role</div>\n <div class=\"po-field-value\">\n {{roleLabel}}\n {{#model.is_staff|bool}}<span class=\"po-badge-muted\">Staff</span>{{/model.is_staff|bool}}\n </div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">MFA</div>\n <div class=\"po-field-value\">\n {{#model.requires_mfa|bool}}<span class=\"po-badge-ok\">Required</span>{{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}<span class=\"po-badge-muted\">Not required</span>{{/model.requires_mfa|bool}}\n </div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Member Since</div>\n <div class=\"po-field-value\">{{model.date_joined|date}}</div>\n </div>\n <div class=\"po-field-row\">\n <div class=\"po-field-label\">Last Login</div>\n <div class=\"po-field-value\">{{model.last_login|relative}}</div>\n </div>\n\n <!-- Permissions peek -->\n <div class=\"po-section-label\">Permissions</div>\n <div class=\"po-perms-peek\">\n {{#permissionPeek}}\n {{#items}}\n <span class=\"po-perm-pill\">{{.}}</span>\n {{/items}}\n {{#remaining|bool}}\n <span class=\"po-perm-more\" data-action=\"navigate\" data-section=\"permissions\">+{{remaining}} more</span>\n {{/remaining|bool}}\n {{/permissionPeek}}\n {{^permissionPeek|bool}}\n <span class=\"po-not-set\">No permissions assigned</span>\n {{/permissionPeek|bool}}\n </div>\n\n <!-- Danger zone -->\n <div style=\"margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #f0f0f0;\">\n <button type=\"button\" class=\"btn btn-link text-danger p-0\" style=\"font-size: 0.8rem; text-decoration: none;\" data-action=\"deactivate-account\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>Deactivate Account\n </button>\n </div>\n `,\n ...options\n });\n }\n\n get hasPhone() {\n return !!(this.model && this.model.get('phone_number'));\n }\n\n get roleLabel() {\n if (!this.model) return 'User';\n if (this.model.get('is_superuser')) return 'Superuser';\n return 'User';\n }\n\n get permissionPeek() {\n if (!this.model) return null;\n const perms = this.model.get('permissions');\n if (!perms) return null;\n\n const permMap = {};\n User.PERMISSIONS.forEach(p => { permMap[p.name] = p.label; });\n\n const active = Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => permMap[k] || k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n\n if (active.length === 0) return null;\n\n const PEEK_COUNT = 5;\n return {\n items: active.slice(0, PEEK_COUNT),\n remaining: Math.max(0, active.length - PEEK_COUNT)\n };\n }\n\n async onActionEditUsername() {\n const app = this.getApp();\n const data = await Dialog.showForm({\n title: 'Change Username',\n size: 'sm',\n submitText: 'Change',\n fields: [\n {\n name: 'new_username',\n type: 'text',\n label: 'New Username',\n required: true,\n placeholder: 'Enter new username',\n attributes: { autocomplete: 'off' },\n cols: 12\n },\n {\n name: 'confirm_identity',\n type: 'password',\n label: 'Current Password',\n required: true,\n placeholder: 'Required to confirm identity',\n showToggle: true,\n attributes: { autocomplete: 'off' },\n cols: 12\n }\n ]\n });\n if (!data) return true;\n\n const resp = await rest.POST('/api/auth/username/change', {\n username: data.new_username,\n current_password: data.confirm_identity\n });\n if (resp.success) {\n app?.toast?.success('Username updated');\n await this.model.fetch({ params: { graph: 'full' } });\n await this.render();\n // Re-render parent to update header\n if (this.parent) await this.parent.render();\n } else {\n app?.toast?.error(resp.message || 'Failed to change username');\n }\n return true;\n }\n\n async onActionDeactivateAccount() {\n const app = this.getApp();\n const confirmed = await Dialog.confirm(\n 'Are you sure you want to deactivate your account? A confirmation email will be sent to complete the process. This action cannot be undone.',\n 'Deactivate Account'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/account/deactivate');\n if (resp.success) {\n app?.toast?.success('A confirmation email has been sent. Follow the link to complete deactivation.');\n } else {\n app?.toast?.error(resp.message || 'Failed to request deactivation');\n }\n return true;\n }\n\n async onActionVerifyEmail() {\n const app = this.getApp();\n const email = this.model.get('email');\n\n // Step 1: Send verification code\n const sendResp = await rest.POST('/api/auth/verify/email/send', { method: 'code' });\n if (!sendResp.success) {\n app?.toast?.error(sendResp.message || 'Failed to send verification code');\n return true;\n }\n\n // Step 2: Prompt for code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${email}</strong>`,\n 'Verify Email',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 3: Confirm\n const confirmResp = await rest.POST('/api/auth/verify/email/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Email verified');\n this.model.set('is_email_verified', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n\n async onActionVerifyPhone() {\n const app = this.getApp();\n const phone = this.model.get('phone_number');\n\n // Step 1: Send verification code\n const sendResp = await rest.POST('/api/auth/verify/phone/send');\n if (!sendResp.success) {\n app?.toast?.error(sendResp.message || 'Failed to send verification code');\n return true;\n }\n\n // Step 2: Prompt for code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${phone}</strong>`,\n 'Verify Phone',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 3: Confirm\n const confirmResp = await rest.POST('/api/auth/verify/phone/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Phone verified');\n this.model.set('is_phone_verified', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n\n async onActionAddPhone() {\n const app = this.getApp();\n\n // Step 1: Collect phone number\n const phone = await Dialog.prompt(\n 'Enter your phone number in E.164 format:',\n 'Add Phone Number',\n { placeholder: '+14155550123' }\n );\n if (!phone || !phone.trim()) return true;\n\n // Step 2: Save to profile\n const saveResp = await this.model.save({ phone_number: phone.trim() });\n if (saveResp.status !== 200) {\n app?.toast?.error(saveResp.message || 'Failed to save phone number');\n return true;\n }\n\n // Step 3: Send verification code\n const sendResp = await rest.POST('/api/auth/verify/phone/send');\n if (!sendResp.success) {\n app?.toast?.error(sendResp.message || 'Failed to send verification code');\n await this.render();\n return true;\n }\n\n // Step 4: Prompt for code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${phone.trim()}</strong>`,\n 'Verify Phone',\n { placeholder: '000000' }\n );\n if (!code) {\n await this.render();\n return true;\n }\n\n // Step 5: Confirm\n const confirmResp = await rest.POST('/api/auth/verify/phone/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Phone number added and verified');\n this.model.set('is_phone_verified', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid or expired code');\n await this.render();\n }\n return true;\n }\n\n async onActionRemovePhone() {\n const app = this.getApp();\n const confirmed = await Dialog.confirm(\n 'Remove your phone number? You will need to add it again to use phone-based verification.',\n 'Remove Phone'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ phone_number: null });\n if (resp.status === 200) {\n app?.toast?.success('Phone number removed');\n this.model.set('is_phone_verified', false);\n await this.render();\n } else {\n app?.toast?.error(resp.message || 'Failed to remove phone number');\n }\n return true;\n }\n\n async onActionNavigate(event, el) {\n // Bubble up to parent UserProfileView\n if (this.parent && this.parent.onActionNavigate) {\n return this.parent.onActionNavigate(event, el);\n }\n return true;\n }\n}\n","/**\n * ProfilePersonalSection - Personal information tab\n *\n * Editable fields: first name, last name, DOB, timezone, address.\n * DOB is a top-level user field with is_dob_verified badge.\n * Address fields are stored in user.metadata.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\n\nexport default class ProfilePersonalSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-personal-section',\n template: `\n <style>\n .pp-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .pp-section-label:first-child { margin-top: 0; }\n .pp-field-row { display: flex; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #f0f0f0; }\n .pp-field-row:last-child { border-bottom: none; }\n .pp-field-label { width: 130px; font-size: 0.8rem; color: #6c757d; flex-shrink: 0; }\n .pp-field-value { flex: 1; font-size: 0.88rem; color: #212529; display: flex; align-items: center; gap: 0.4rem; }\n .pp-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .pp-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n .pp-badge-ok { font-size: 0.65rem; padding: 0.15em 0.45em; background: #d1e7dd; color: #0f5132; border-radius: 3px; }\n .pp-badge-warn { font-size: 0.65rem; padding: 0.15em 0.45em; background: #fff3cd; color: #856404; border-radius: 3px; }\n .pp-not-set { color: #adb5bd; font-style: italic; font-size: 0.85rem; }\n </style>\n\n <!-- Name -->\n <div class=\"pp-section-label\">Name</div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Display Name</div>\n <div class=\"pp-field-value\">\n {{#model.display_name}}{{model.display_name}}{{/model.display_name}}\n {{^model.display_name}}<span class=\"pp-not-set\">Not set</span>{{/model.display_name}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-display-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">First Name</div>\n <div class=\"pp-field-value\">\n {{#model.first_name}}{{model.first_name}}{{/model.first_name}}\n {{^model.first_name}}<span class=\"pp-not-set\">Not set</span>{{/model.first_name}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-first-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Last Name</div>\n <div class=\"pp-field-value\">\n {{#model.last_name}}{{model.last_name}}{{/model.last_name}}\n {{^model.last_name}}<span class=\"pp-not-set\">Not set</span>{{/model.last_name}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-last-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n\n <!-- Details -->\n <div class=\"pp-section-label\">Details</div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Date of Birth</div>\n <div class=\"pp-field-value\">\n {{#hasDob|bool}}\n {{dobFormatted}}\n {{#model.is_dob_verified|bool}}<span class=\"pp-badge-ok\">Verified</span>{{/model.is_dob_verified|bool}}\n {{^model.is_dob_verified|bool}}<span class=\"pp-badge-warn\">Unverified</span>{{/model.is_dob_verified|bool}}\n {{/hasDob|bool}}\n {{^hasDob|bool}}<span class=\"pp-not-set\">Not set</span>{{/hasDob|bool}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-dob\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Timezone</div>\n <div class=\"pp-field-value\">{{timezoneDisplay}}</div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-timezone\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n\n <!-- Address -->\n <div class=\"pp-section-label\">Address</div>\n <div class=\"pp-field-row\">\n <div class=\"pp-field-label\">Address</div>\n <div class=\"pp-field-value\">\n {{#hasAddress|bool}}\n {{addressSummary}}\n {{/hasAddress|bool}}\n {{^hasAddress|bool}}\n <span class=\"pp-not-set\">Not set</span>\n {{/hasAddress|bool}}\n </div>\n <button type=\"button\" class=\"pp-field-action\" data-action=\"edit-address\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n `,\n ...options\n });\n }\n\n get hasDob() {\n return !!this.model?.get('dob');\n }\n\n get dobFormatted() {\n const dob = this.model?.get('dob');\n if (!dob) return '';\n try {\n const [year, month, day] = dob.split('-');\n return `${month}/${day}/${year}`;\n } catch {\n return dob;\n }\n }\n\n get timezoneDisplay() {\n const meta = this.model?.get('metadata') || {};\n return meta.timezone || 'Not set';\n }\n\n get hasAddress() {\n const meta = this.model?.get('metadata') || {};\n return !!(meta.street || meta.city || meta.state || meta.zip || meta.country);\n }\n\n get addressSummary() {\n const meta = this.model?.get('metadata') || {};\n const parts = [meta.street, meta.city, meta.state, meta.zip, meta.country].filter(Boolean);\n return parts.join(', ');\n }\n\n async onActionEditDisplayName() {\n const name = await Dialog.prompt(\n 'Enter your display name:',\n 'Display Name',\n { defaultValue: this.model.get('display_name') || '' }\n );\n if (name !== null && name.trim()) {\n const resp = await this.model.save({ display_name: name.trim() });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Display name updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update display name');\n }\n }\n return true;\n }\n\n async onActionEditFirstName() {\n const name = await Dialog.prompt(\n 'Enter your first name:',\n 'First Name',\n { defaultValue: this.model.get('first_name') || '' }\n );\n if (name !== null) {\n const resp = await this.model.save({ first_name: name.trim() });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('First name updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update first name');\n }\n }\n return true;\n }\n\n async onActionEditLastName() {\n const name = await Dialog.prompt(\n 'Enter your last name:',\n 'Last Name',\n { defaultValue: this.model.get('last_name') || '' }\n );\n if (name !== null) {\n const resp = await this.model.save({ last_name: name.trim() });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Last name updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update last name');\n }\n }\n return true;\n }\n\n async onActionEditDob() {\n const data = await Dialog.showForm({\n title: 'Date of Birth',\n size: 'sm',\n fields: [{\n name: 'dob',\n type: 'date',\n label: 'Date of Birth',\n cols: 12\n }],\n data: { dob: this.model.get('dob') || '' }\n });\n if (!data) return true;\n\n const resp = await this.model.save({ dob: data.dob || null });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Date of birth updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update date of birth');\n }\n return true;\n }\n\n async onActionEditTimezone() {\n const meta = this.model.get('metadata') || {};\n const data = await Dialog.showForm({\n title: 'Change Timezone',\n fields: [{\n name: 'timezone',\n type: 'select',\n label: 'Timezone',\n cols: 12,\n options: [\n { value: 'America/New_York', text: 'Eastern Time (ET)' },\n { value: 'America/Chicago', text: 'Central Time (CT)' },\n { value: 'America/Denver', text: 'Mountain Time (MT)' },\n { value: 'America/Los_Angeles', text: 'Pacific Time (PT)' },\n { value: 'America/Anchorage', text: 'Alaska Time (AKT)' },\n { value: 'Pacific/Honolulu', text: 'Hawaii Time (HT)' },\n { value: 'UTC', text: 'UTC' },\n { value: 'Europe/London', text: 'London (GMT/BST)' },\n { value: 'Europe/Paris', text: 'Paris (CET/CEST)' },\n { value: 'Europe/Berlin', text: 'Berlin (CET/CEST)' },\n { value: 'Asia/Tokyo', text: 'Tokyo (JST)' },\n { value: 'Asia/Shanghai', text: 'Shanghai (CST)' },\n { value: 'Australia/Sydney', text: 'Sydney (AEST)' }\n ]\n }],\n data: { timezone: meta.timezone || '' },\n size: 'sm'\n });\n if (!data) return true;\n\n const updatedMeta = { ...meta, timezone: data.timezone };\n const resp = await this.model.save({ metadata: updatedMeta });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Timezone updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update timezone');\n }\n return true;\n }\n\n async onActionEditAddress() {\n const meta = this.model.get('metadata') || {};\n const data = await Dialog.showForm({\n title: 'Edit Address',\n size: 'md',\n fields: [\n { name: 'street', type: 'text', label: 'Street', placeholder: '123 Main St', cols: 12 },\n { name: 'city', type: 'text', label: 'City', cols: 6 },\n { name: 'state', type: 'text', label: 'State / Province', cols: 6 },\n { name: 'zip', type: 'text', label: 'Zip / Postal Code', cols: 6 },\n { name: 'country', type: 'text', label: 'Country', cols: 6 }\n ],\n data: {\n street: meta.street || '',\n city: meta.city || '',\n state: meta.state || '',\n zip: meta.zip || '',\n country: meta.country || ''\n }\n });\n\n if (!data) return true;\n\n const updatedMeta = {\n ...meta,\n street: data.street || '',\n city: data.city || '',\n state: data.state || '',\n zip: data.zip || '',\n country: data.country || ''\n };\n const resp = await this.model.save({ metadata: updatedMeta });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Address updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update address');\n }\n return true;\n }\n}\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport rest from '@core/Rest.js';\n\n/**\n * Passkey - WebAuthn/FIDO2 passkey model\n * Maps to REST endpoints under /api/account/passkeys\n *\n * Key operations:\n * - List/View/Update/Delete passkeys (standard CRUD)\n * - Register new passkeys via WebAuthn flow\n *\n * Notes:\n * - Registration is a two-step process: begin → complete\n * - Login flow is NOT handled here (separate auth flow)\n * - Passkeys are portal-specific (rp_id = domain)\n * - Most fields are read-only; only friendly_name and is_enabled are editable\n */\nclass Passkey extends Model {\n constructor(data = {}, options = {}) {\n super(data, {\n endpoint: '/api/account/passkeys',\n ...options\n });\n }\n\n /**\n * Begin passkey registration flow.\n * POST /api/account/passkeys/register/begin\n *\n * Returns WebAuthn challenge and publicKey options for navigator.credentials.create()\n *\n * @param {object} options - Optional { params?: object }\n * @returns {Promise<object>} REST response with challenge_id and publicKey options\n *\n * Example response:\n * {\n * status: true,\n * data: {\n * challenge_id: \"uuid...\",\n * expiresAt: \"2025-02-05T18:27:10Z\",\n * publicKey: {\n * rp: { name: \"MOJO\", id: \"portal.example.com\" },\n * user: { name: \"username\", displayName: \"User\", id: \"...\" },\n * challenge: \"base64...\",\n * pubKeyCredParams: [...],\n * authenticatorSelection: {...}\n * }\n * }\n * }\n */\n static async registerBegin(options = {}) {\n try {\n const url = '/api/account/passkeys/register/begin';\n return await rest.POST(url, {}, options.params);\n } catch (err) {\n return {\n success: false,\n status: err?.status || 500,\n error: err?.message || 'Failed to begin passkey registration'\n };\n }\n }\n\n /**\n * Complete passkey registration flow.\n * POST /api/account/passkeys/register/complete\n *\n * Submits the WebAuthn credential created by navigator.credentials.create()\n *\n * @param {object} data\n * challenge_id: string (from registerBegin response)\n * credential: object (from navigator.credentials.create)\n * friendly_name: string (optional, e.g., \"My iPhone\")\n * @param {object} options - Optional { params?: object }\n * @returns {Promise<object>} REST response with created Passkey object\n *\n * Example data:\n * {\n * challenge_id: \"uuid...\",\n * credential: {\n * id: \"...\",\n * rawId: \"...\",\n * type: \"public-key\",\n * response: {\n * clientDataJSON: \"...\",\n * attestationObject: \"...\"\n * },\n * transports: [\"internal\"]\n * },\n * friendly_name: \"My MacBook\"\n * }\n */\n static async registerComplete(data = {}, options = {}) {\n if (!data.challenge_id) {\n return {\n success: false,\n status: 400,\n error: 'Missing challenge_id'\n };\n }\n\n if (!data.credential) {\n return {\n success: false,\n status: 400,\n error: 'Missing credential data'\n };\n }\n\n try {\n const url = '/api/account/passkeys/register/complete';\n return await rest.POST(url, data, options.params);\n } catch (err) {\n return {\n success: false,\n status: err?.status || 500,\n error: err?.message || 'Failed to complete passkey registration'\n };\n }\n }\n}\n\n/**\n * PasskeyList - Collection of Passkey\n * Supports standard MOJO list/search/sort/pagination patterns\n */\nclass PasskeyList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: Passkey,\n endpoint: '/api/account/passkeys',\n size: 10,\n ...options\n });\n }\n}\n\n/**\n * Forms configuration for Passkey\n *\n * Notes:\n * - No create form (registration uses WebAuthn flow)\n * - Edit form allows changing friendly_name and is_enabled only\n * - View form shows all fields as read-only for informational purposes\n */\nconst PasskeyForms = {\n edit: {\n title: 'Edit Passkey',\n fields: [\n {\n name: 'friendly_name',\n type: 'text',\n label: 'Name',\n placeholder: 'My iPhone',\n required: true,\n columns: 12,\n help: 'A friendly name to identify this passkey'\n },\n {\n name: 'is_enabled',\n type: 'switch',\n label: 'Enabled',\n columns: 12,\n help: 'Disable to prevent this passkey from being used for authentication'\n }\n ]\n },\n\n view: {\n title: 'Passkey Details',\n fields: [\n {\n name: 'friendly_name',\n type: 'text',\n label: 'Name',\n readonly: true,\n columns: 12\n },\n {\n name: 'is_enabled',\n type: 'switch',\n label: 'Enabled',\n readonly: true,\n columns: 6\n },\n {\n name: 'rp_id',\n type: 'text',\n label: 'Portal (RP ID)',\n readonly: true,\n columns: 6,\n help: 'The portal/domain this passkey is registered for'\n },\n {\n name: 'aaguid',\n type: 'text',\n label: 'Authenticator GUID',\n readonly: true,\n columns: 12,\n help: 'Unique identifier for the authenticator device'\n },\n {\n name: 'transports',\n type: 'text',\n label: 'Transports',\n readonly: true,\n columns: 6,\n help: 'Available transport methods (e.g., internal, usb, nfc, ble)'\n },\n {\n name: 'sign_count',\n type: 'number',\n label: 'Signature Count',\n readonly: true,\n columns: 6,\n help: 'Number of times this passkey has been used (for clone detection)'\n },\n {\n name: 'last_used',\n type: 'datetime',\n label: 'Last Used',\n readonly: true,\n columns: 6\n },\n {\n name: 'created',\n type: 'datetime',\n label: 'Created',\n readonly: true,\n columns: 6\n }\n ]\n }\n};\n\nexport {\n Passkey,\n PasskeyList,\n PasskeyForms\n};\n","/**\n * ProfileSecuritySection - Security dashboard tab\n *\n * Compact card rows. Password and Passkeys are actions (dialog/flow).\n * Sessions, Devices, Activity navigate to their own nav sections.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\nimport { Passkey, PasskeyList, PasskeyForms } from '@core/models/Passkeys.js';\n\nexport default class ProfileSecuritySection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-security-section',\n template: `\n <style>\n .ps-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .ps-section-label:first-child { margin-top: 0; }\n .ps-item { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.5rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }\n .ps-item:hover { border-color: #dee2e6; background: #fafbfd; }\n .ps-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; }\n .ps-info { flex: 1; min-width: 0; }\n .ps-title { font-weight: 600; font-size: 0.88rem; }\n .ps-desc { font-size: 0.78rem; color: #6c757d; }\n .ps-badge { font-size: 0.72rem; padding: 0.15em 0.5em; border-radius: 3px; flex-shrink: 0; }\n .ps-chevron { color: #ced4da; font-size: 0.8rem; flex-shrink: 0; }\n </style>\n\n <div class=\"ps-section-label\">Authentication</div>\n\n <div class=\"ps-item\" data-action=\"change-password\">\n <div class=\"ps-icon bg-primary bg-opacity-10 text-primary\"><i class=\"bi bi-lock\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Password</div>\n <div class=\"ps-desc\">Change your account password</div>\n </div>\n <span class=\"ps-badge bg-light text-muted border\">Change</span>\n </div>\n\n <div class=\"ps-item\" data-action=\"manage-passkeys\">\n <div class=\"ps-icon bg-success bg-opacity-10 text-success\"><i class=\"bi bi-fingerprint\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Passkeys</div>\n <div class=\"ps-desc\">Passwordless sign-in with biometrics</div>\n </div>\n <i class=\"bi bi-chevron-right ps-chevron\"></i>\n </div>\n\n <div class=\"ps-item\" data-action=\"manage-totp\">\n <div class=\"ps-icon bg-purple bg-opacity-10\" style=\"background: rgba(111,66,193,0.1); color: #6f42c1;\"><i class=\"bi bi-shield-lock\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Authenticator App</div>\n <div class=\"ps-desc\">Two-factor authentication with TOTP codes</div>\n </div>\n {{#model.requires_mfa|bool}}\n <span class=\"ps-badge bg-success bg-opacity-10 text-success border\">Enabled</span>\n {{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}\n <span class=\"ps-badge bg-light text-muted border\">Setup</span>\n {{/model.requires_mfa|bool}}\n </div>\n\n {{#model.requires_mfa|bool}}\n <div class=\"ps-item\" data-action=\"manage-recovery-codes\">\n <div class=\"ps-icon\" style=\"background: rgba(111,66,193,0.1); color: #6f42c1;\"><i class=\"bi bi-file-earmark-lock\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Recovery Codes</div>\n <div class=\"ps-desc\">Backup codes for when you lose your authenticator</div>\n </div>\n <i class=\"bi bi-chevron-right ps-chevron\"></i>\n </div>\n {{/model.requires_mfa|bool}}\n\n <div class=\"ps-section-label\">Sessions</div>\n\n <div class=\"ps-item\" data-action=\"revoke-all-sessions\">\n <div class=\"ps-icon\" style=\"background: rgba(220,53,69,0.1); color: #dc3545;\"><i class=\"bi bi-box-arrow-right\"></i></div>\n <div class=\"ps-info\">\n <div class=\"ps-title\">Revoke All Sessions</div>\n <div class=\"ps-desc\">Sign out of all devices except this one</div>\n </div>\n </div>\n `,\n ...options\n });\n }\n\n // --- Actions ---\n\n async onActionChangePassword() {\n const app = this.getApp();\n if (app && app.changePassword) {\n await app.changePassword();\n }\n return true;\n }\n\n async onActionManagePasskeys() {\n const collection = new PasskeyList({ params: { user: this.model.id } });\n try {\n await collection.fetch();\n } catch (e) {\n // ignore fetch errors\n }\n\n const items = collection.models || [];\n const view = new View({\n template: `\n <style>\n .pk-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0.75rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.4rem; }\n .pk-icon { width: 32px; height: 32px; background: #e7f1ff; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #0d6efd; font-size: 0.9rem; flex-shrink: 0; }\n .pk-info { flex: 1; min-width: 0; }\n .pk-name { font-weight: 600; font-size: 0.85rem; }\n .pk-meta { font-size: 0.73rem; color: #6c757d; }\n .pk-actions { display: flex; gap: 0.25rem; }\n .pk-actions .btn { padding: 0.2rem 0.4rem; font-size: 0.75rem; }\n .pk-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pk-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n {{#passkeys}}\n <div class=\"pk-row\">\n <div class=\"pk-icon\"><i class=\"bi bi-fingerprint\"></i></div>\n <div class=\"pk-info\">\n <div class=\"pk-name\">{{.friendly_name|default:'Unnamed Passkey'}}</div>\n <div class=\"pk-meta\">Created {{.created|date}} · Last used {{.last_used|relative|default:'never'}} · {{.sign_count}} uses</div>\n </div>\n <div class=\"pk-actions\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"edit-passkey\" data-id=\"{{.id}}\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"delete-passkey\" data-id=\"{{.id}}\" title=\"Delete\"><i class=\"bi bi-trash\"></i></button>\n </div>\n </div>\n {{/passkeys}}\n {{^passkeys|bool}}\n <div class=\"pk-empty\">\n <i class=\"bi bi-fingerprint\"></i>\n No passkeys registered yet\n </div>\n {{/passkeys|bool}}\n `\n });\n view.passkeys = items.map(p => p.toJSON ? p.toJSON() : p);\n\n view.onActionEditPasskey = async (event, el) => {\n const id = el.dataset.id;\n const passkey = items.find(p => String(p.id) === String(id));\n if (passkey) {\n await Dialog.showModelForm({\n title: 'Edit Passkey',\n model: passkey,\n fields: PasskeyForms.edit.fields,\n size: 'sm'\n });\n }\n return true;\n };\n\n view.onActionDeletePasskey = async (event, el) => {\n const id = el.dataset.id;\n const confirmed = await Dialog.confirm('Delete this passkey? You won\\'t be able to use it to sign in anymore.', 'Delete Passkey');\n if (confirmed) {\n const passkey = items.find(p => String(p.id) === String(id));\n if (passkey) {\n await passkey.destroy();\n this.getApp()?.toast?.success('Passkey deleted');\n }\n }\n return true;\n };\n\n const result = await Dialog.showDialog({\n title: 'Passkeys',\n body: view,\n size: 'md',\n buttons: [\n { text: 'Add Passkey', icon: 'bi-plus-lg', class: 'btn-primary', value: 'add' },\n { text: 'Close', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n\n if (result === 'add') {\n const added = await this._registerPasskey();\n if (added) {\n this.getApp()?.toast?.success('Passkey registered successfully');\n }\n }\n return true;\n }\n\n _base64urlToBytes(base64url) {\n const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);\n return Uint8Array.from(atob(padded), c => c.charCodeAt(0));\n }\n\n async _registerPasskey() {\n try {\n const beginResp = await Passkey.registerBegin();\n if (!beginResp.success || !beginResp.data) {\n this.getApp()?.toast?.error('Failed to start passkey registration');\n return;\n }\n\n const options = beginResp.data.data || beginResp.data;\n const publicKey = options.publicKey;\n\n // Override rp.id to match current domain (server may return production domain)\n if (publicKey.rp) {\n publicKey.rp.id = window.location.hostname;\n }\n\n if (publicKey.challenge && typeof publicKey.challenge === 'string') {\n publicKey.challenge = this._base64urlToBytes(publicKey.challenge);\n }\n if (publicKey.user && publicKey.user.id && typeof publicKey.user.id === 'string') {\n publicKey.user.id = this._base64urlToBytes(publicKey.user.id);\n }\n if (publicKey.excludeCredentials) {\n publicKey.excludeCredentials = publicKey.excludeCredentials.map(cred => ({\n ...cred,\n id: typeof cred.id === 'string' ? this._base64urlToBytes(cred.id) : cred.id\n }));\n }\n\n const credential = await navigator.credentials.create({ publicKey });\n if (!credential) {\n this.getApp()?.toast?.error('Passkey creation was cancelled');\n return;\n }\n\n const friendlyName = await Dialog.prompt('Name this passkey:', 'Passkey Name', {\n defaultValue: '',\n placeholder: 'e.g., My MacBook'\n });\n\n const credentialData = {\n id: credential.id,\n rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),\n type: credential.type,\n response: {\n clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),\n attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject)))\n }\n };\n\n if (credential.response.getTransports) {\n credentialData.transports = credential.response.getTransports();\n }\n\n const completeResp = await Passkey.registerComplete({\n challenge_id: options.challenge_id,\n credential: credentialData,\n friendly_name: friendlyName || 'My Passkey'\n });\n\n if (completeResp.success) {\n return true;\n } else {\n this.getApp()?.toast?.error(completeResp.error || 'Failed to register passkey');\n return false;\n }\n } catch (err) {\n if (err.name === 'NotAllowedError') {\n this.getApp()?.toast?.error('Passkey creation was blocked or cancelled. Check your browser settings if this persists.');\n return false;\n }\n if (err.name === 'SecurityError') {\n this.getApp()?.toast?.error('Passkeys are not supported on this domain');\n } else {\n console.error('Passkey registration error:', err);\n this.getApp()?.toast?.error('Passkey registration failed');\n }\n return false;\n }\n }\n\n async onActionManageTotp() {\n const app = this.getApp();\n const isMfaEnabled = this.model.get('requires_mfa');\n\n if (isMfaEnabled) {\n // Already enabled — offer to disable\n const confirmed = await Dialog.confirm(\n 'Disable authenticator app? You will no longer need a TOTP code to sign in.',\n 'Disable Authenticator'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE('/api/account/totp');\n if (resp.success) {\n app?.toast?.success('Authenticator app disabled');\n this.model.set('requires_mfa', false);\n await this.render();\n } else {\n app?.toast?.error(resp.message || 'Failed to disable authenticator');\n }\n return true;\n }\n\n // Step 1: Call setup to get QR code + secret\n const setupResp = await rest.POST('/api/account/totp/setup', {}, {}, { dataOnly: true });\n if (!setupResp.success || !setupResp.data) {\n app?.toast?.error(setupResp.message || 'Failed to start authenticator setup');\n return true;\n }\n\n const { secret, qr_code } = setupResp.data;\n\n // Step 2: Show QR code dialog\n const setupView = new View({\n template: `\n <div style=\"text-align: center; padding: 0.5rem 0;\">\n <p style=\"font-size: 0.85rem; color: #6c757d; margin-bottom: 1rem;\">\n Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.)\n </p>\n <img src=\"{{{qrCode}}}\" alt=\"TOTP QR Code\" style=\"width: 200px; height: 200px; margin: 0 auto; display: block; border: 1px solid #e9ecef; border-radius: 8px; padding: 8px;\" />\n <div style=\"margin-top: 1rem;\">\n <p style=\"font-size: 0.75rem; color: #adb5bd; margin-bottom: 0.25rem;\">Or enter this key manually:</p>\n <code style=\"font-size: 0.85rem; letter-spacing: 0.1em; user-select: all; padding: 0.35rem 0.75rem; background: #f8f9fa; border-radius: 4px;\">{{secret}}</code>\n </div>\n </div>\n `\n });\n setupView.qrCode = qr_code;\n setupView.secret = secret;\n\n const result = await Dialog.showDialog({\n title: 'Set Up Authenticator',\n body: setupView,\n size: 'sm',\n buttons: [\n { text: 'Cancel', class: 'btn-secondary', dismiss: true },\n { text: 'Next', class: 'btn-primary', value: 'next' }\n ]\n });\n\n if (result !== 'next') return true;\n\n // Step 3: Prompt for the first TOTP code to confirm\n const code = await Dialog.prompt(\n 'Enter the 6-digit code from your authenticator app to verify setup:',\n 'Verify Authenticator',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 4: Confirm\n const confirmResp = await rest.POST('/api/account/totp/confirm', { code: code.trim() });\n if (confirmResp.success) {\n app?.toast?.success('Authenticator app enabled');\n this.model.set('requires_mfa', true);\n await this.render();\n } else {\n app?.toast?.error(confirmResp.message || 'Invalid code. Please try setup again.');\n }\n return true;\n }\n\n // Navigate to sections in the parent UserProfileView\n async onActionManageRecoveryCodes() {\n const app = this.getApp();\n\n // Fetch masked recovery codes\n const resp = await rest.GET('/api/account/totp/recovery-codes', {}, { dataOnly: true });\n if (!resp.success || !resp.data) {\n app?.toast?.error(resp.message || 'Failed to load recovery codes');\n return true;\n }\n\n const { remaining, codes } = resp.data;\n\n const view = new View({\n template: `\n <style>\n .rc-info { font-size: 0.82rem; color: #6c757d; margin-bottom: 1rem; }\n .rc-remaining { font-weight: 600; color: #495057; }\n .rc-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; }\n .rc-code { font-family: monospace; font-size: 0.85rem; padding: 0.35rem 0.6rem; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; text-align: center; }\n </style>\n <div class=\"rc-info\">\n <span class=\"rc-remaining\">{{remaining}}</span> recovery codes remaining\n </div>\n <div class=\"rc-list\">\n {{#codes}}\n <div class=\"rc-code\">{{.}}</div>\n {{/codes}}\n </div>\n `\n });\n view.remaining = remaining;\n view.codes = codes || [];\n\n const result = await Dialog.showDialog({\n title: 'Recovery Codes',\n body: view,\n size: 'sm',\n buttons: [\n { text: 'Regenerate', icon: 'bi-arrow-repeat', class: 'btn-warning', value: 'regenerate' },\n { text: 'Close', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n\n if (result === 'regenerate') {\n // Require current TOTP code to regenerate\n const code = await Dialog.prompt(\n 'Enter your current authenticator code to regenerate recovery codes:',\n 'Regenerate Recovery Codes',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n const regenResp = await rest.POST('/api/account/totp/recovery-codes/regenerate', {\n code: code.trim()\n }, {}, { dataOnly: true });\n\n if (regenResp.success && regenResp.data?.recovery_codes) {\n const newCodes = regenResp.data.recovery_codes;\n const codesText = newCodes.join('\\n');\n\n const newView = new View({\n template: `\n <style>\n .rc-warning { padding: 0.65rem 0.85rem; background: #fff3cd; border: 1px solid #ffecb5; border-radius: 6px; margin-bottom: 1rem; font-size: 0.8rem; color: #664d03; }\n .rc-new-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; margin-bottom: 1rem; }\n .rc-new-code { font-family: monospace; font-size: 0.85rem; padding: 0.35rem 0.6rem; background: #d1e7dd; border: 1px solid #badbcc; border-radius: 4px; text-align: center; }\n </style>\n <div class=\"rc-warning\">\n <i class=\"bi bi-exclamation-triangle me-1\"></i>\n <strong>Save these codes now.</strong> They will not be shown again. Old codes are invalidated.\n </div>\n <div class=\"rc-new-list\">\n {{#newCodes}}\n <div class=\"rc-new-code\">{{.}}</div>\n {{/newCodes}}\n </div>\n `\n });\n newView.newCodes = newCodes;\n\n await Dialog.showDialog({\n title: 'New Recovery Codes',\n body: newView,\n size: 'sm',\n buttons: [\n { text: 'Copy All', icon: 'bi-clipboard', class: 'btn-primary', handler: async () => {\n try {\n await navigator.clipboard.writeText(codesText);\n app?.toast?.success('Recovery codes copied');\n } catch {\n app?.toast?.error('Failed to copy codes');\n }\n return false; // keep dialog open\n }},\n { text: 'Done', class: 'btn-outline-secondary', dismiss: true }\n ]\n });\n } else {\n app?.toast?.error(regenResp.message || 'Failed to regenerate recovery codes');\n }\n }\n return true;\n }\n\n async onActionRevokeAllSessions() {\n const app = this.getApp();\n\n const confirmed = await Dialog.confirm(\n 'This will sign you out of all other devices and sessions. Your current session will remain active. This cannot be undone.',\n 'Revoke All Sessions'\n );\n if (!confirmed) return true;\n\n const password = await Dialog.prompt(\n 'Enter your current password to confirm:',\n 'Confirm Password',\n { placeholder: 'Current password' }\n );\n if (!password) return true;\n\n const resp = await rest.POST('/api/auth/sessions/revoke', {\n current_password: password.trim()\n }, {}, { dataOnly: true });\n\n if (resp.success && resp.data) {\n // Store new tokens — old ones are now invalid\n if (resp.data.access_token) {\n app?.auth?.setTokens?.(resp.data);\n }\n app?.toast?.success('All other sessions have been revoked');\n } else {\n app?.toast?.error(resp.message || 'Failed to revoke sessions');\n }\n return true;\n }\n\n // Navigate to sections in the parent UserProfileView\n async onActionNavigate(event, el) {\n if (this.parent && this.parent.onActionNavigate) {\n return this.parent.onActionNavigate(event, el);\n }\n return true;\n }\n}\n","/**\n * ProfileConnectedSection - OAuth connected accounts tab\n *\n * Lists OAuth provider connections (Google, GitHub, etc.).\n * Users can unlink connections with lockout guard protection.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\n\nconst PROVIDER_ICONS = {\n google: 'bi-google',\n github: 'bi-github',\n microsoft: 'bi-microsoft',\n apple: 'bi-apple',\n facebook: 'bi-facebook',\n twitter: 'bi-twitter-x',\n linkedin: 'bi-linkedin'\n};\n\nexport default class ProfileConnectedSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-connected-section',\n template: `\n <style>\n .pc-row { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.5rem; }\n .pc-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; background: #f0f0f0; color: #495057; }\n .pc-info { flex: 1; min-width: 0; }\n .pc-provider { font-weight: 600; font-size: 0.88rem; text-transform: capitalize; }\n .pc-meta { font-size: 0.78rem; color: #6c757d; }\n .pc-actions { flex-shrink: 0; }\n .pc-actions .btn { font-size: 0.75rem; padding: 0.25rem 0.5rem; }\n .pc-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pc-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n\n {{#connections}}\n <div class=\"pc-row\">\n <div class=\"pc-icon\"><i class=\"bi {{.icon}}\"></i></div>\n <div class=\"pc-info\">\n <div class=\"pc-provider\">{{.provider}}</div>\n <div class=\"pc-meta\">{{.email}} · Connected {{.created|relative}}</div>\n </div>\n <div class=\"pc-actions\">\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"unlink\" data-id=\"{{.id}}\" title=\"Unlink\"><i class=\"bi bi-x-lg me-1\"></i>Unlink</button>\n </div>\n </div>\n {{/connections}}\n {{^connections|bool}}\n <div class=\"pc-empty\">\n <i class=\"bi bi-plug\"></i>\n No connected accounts\n <div style=\"font-size: 0.78rem; margin-top: 0.5rem;\">\n Connect a social account by signing in with a provider while logged in.\n </div>\n </div>\n {{/connections|bool}}\n `,\n ...options\n });\n this.connections = [];\n }\n\n async onBeforeRender() {\n try {\n const resp = await rest.GET('/api/account/oauth_connection');\n const results = resp?.data?.results || resp?.data || [];\n this.connections = results.map(c => ({\n ...c,\n icon: PROVIDER_ICONS[c.provider] || 'bi-link-45deg'\n }));\n } catch (e) {\n this.connections = [];\n }\n }\n\n async onActionUnlink(event, el) {\n const id = el.dataset.id;\n const connection = this.connections.find(c => String(c.id) === String(id));\n const provider = connection?.provider || 'this account';\n\n const confirmed = await Dialog.confirm(\n `Unlink ${provider}? You won't be able to sign in with this provider anymore.`,\n 'Unlink Account'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE(`/api/account/oauth_connection/${id}`);\n if (resp.success) {\n this.getApp()?.toast?.success(`${provider} account unlinked`);\n await this.render();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to unlink account');\n }\n return true;\n }\n}\n","/**\n * ProfileSessionsSection - Sessions tab\n *\n * Uses ListView + UserDeviceLocationList to show\n * active sessions with browser, device, location info.\n */\nimport View from '@core/View.js';\nimport ListView from '@core/views/list/ListView.js';\nimport ListViewItem from '@core/views/list/ListViewItem.js';\nimport { UserDeviceLocationList } from '@core/models/User.js';\n\nclass SessionItem extends ListViewItem {\n get browserName() {\n const ud = this.model?.get('user_device');\n return ud?.device_info?.user_agent?.family || 'Unknown';\n }\n\n get deviceName() {\n const ud = this.model?.get('user_device');\n const dev = ud?.device_info?.device || {};\n return `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown Device';\n }\n\n get location() {\n const geo = this.model?.get('geolocation') || {};\n const parts = [geo.city, geo.region].filter(Boolean);\n return parts.length ? parts.join(', ') : (geo.country_name || '');\n }\n\n get isMobile() {\n const ud = this.model?.get('user_device');\n const dev = ud?.device_info?.device || {};\n const os = ud?.device_info?.os || {};\n return ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n }\n\n get deviceIcon() {\n return this.isMobile ? 'bi-phone' : 'bi-laptop';\n }\n}\n\nexport default class ProfileSessionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-sessions-section',\n template: `\n <style>\n .pss-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0.75rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.4rem; }\n .pss-icon { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 0.85rem; flex-shrink: 0; background: #f0f0f0; color: #6c757d; }\n .pss-info { flex: 1; min-width: 0; }\n .pss-name { font-weight: 600; font-size: 0.85rem; }\n .pss-meta { font-size: 0.73rem; color: #6c757d; }\n </style>\n <div id=\"sessions-list\"></div>\n `,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.listView = new ListView({\n containerId: 'sessions-list',\n collection: new UserDeviceLocationList({ size: 20 }),\n defaultQuery: { user: this.model.id },\n itemClass: SessionItem,\n itemTemplate: `\n <div class=\"pss-row\">\n <div class=\"pss-icon\"><i class=\"bi {{deviceIcon}}\"></i></div>\n <div class=\"pss-info\">\n <div class=\"pss-name\">{{browserName}} on {{deviceName}}</div>\n <div class=\"pss-meta\">{{location}} · {{model.ip_address}} · Last seen {{model.last_seen|relative}}</div>\n </div>\n </div>\n `,\n emptyMessage: 'No session data available'\n });\n this.addChild(this.listView);\n }\n}\n","/**\n * ProfileDevicesSection - Devices tab\n *\n * Uses ListView + UserDeviceList to show\n * registered devices with browser, OS, IP info.\n */\nimport View from '@core/View.js';\nimport ListView from '@core/views/list/ListView.js';\nimport ListViewItem from '@core/views/list/ListViewItem.js';\nimport { UserDeviceList } from '@core/models/User.js';\n\nclass DeviceItem extends ListViewItem {\n get deviceName() {\n const info = this.model?.get('device_info') || {};\n const dev = info.device || {};\n return `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown Device';\n }\n\n get browserInfo() {\n const info = this.model?.get('device_info') || {};\n const ua = info.user_agent || {};\n return ua.family ? `${ua.family} ${ua.major || ''}`.trim() : 'Unknown Browser';\n }\n\n get osName() {\n const info = this.model?.get('device_info') || {};\n return info.os?.family || '';\n }\n\n get isMobile() {\n const info = this.model?.get('device_info') || {};\n const dev = info.device || {};\n const os = info.os || {};\n return ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n }\n\n get deviceIcon() {\n return this.isMobile ? 'bi-phone' : 'bi-laptop';\n }\n}\n\nexport default class ProfileDevicesSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-devices-section',\n template: `\n <style>\n .pd-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0.75rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.4rem; }\n .pd-icon { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 0.85rem; flex-shrink: 0; background: #f0f0f0; color: #495057; }\n .pd-info { flex: 1; min-width: 0; }\n .pd-name { font-weight: 600; font-size: 0.85rem; }\n .pd-meta { font-size: 0.73rem; color: #6c757d; }\n </style>\n <div id=\"devices-list\"></div>\n `,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.listView = new ListView({\n containerId: 'devices-list',\n collection: new UserDeviceList({ size: 20 }),\n defaultQuery: { user: this.model.id },\n itemClass: DeviceItem,\n itemTemplate: `\n <div class=\"pd-row\">\n <div class=\"pd-icon\"><i class=\"bi {{deviceIcon}}\"></i></div>\n <div class=\"pd-info\">\n <div class=\"pd-name\">{{deviceName}}</div>\n <div class=\"pd-meta\">{{browserInfo}} · {{osName}} · {{model.last_ip}} · Last seen {{model.last_seen|relative}}</div>\n </div>\n </div>\n `,\n emptyMessage: 'No registered devices'\n });\n this.addChild(this.listView);\n }\n}\n","/**\n * ProfileSecurityEventsSection - Security events feed\n *\n * TableView showing auth-relevant events: logins, failed attempts,\n * password changes, etc. Color-coded by severity.\n */\nimport View from '@core/View.js';\nimport Model from '@core/Model.js';\nimport Collection from '@core/Collection.js';\nimport TableView from '@core/views/table/TableView.js';\nimport TableRow from '@core/views/table/TableRow.js';\n\nclass SecurityEvent extends Model {\n constructor(data = {}, options = {}) {\n super(data, { endpoint: '/api/account/security-events', ...options });\n }\n}\n\nclass SecurityEventList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: SecurityEvent,\n endpoint: '/api/account/security-events',\n size: 15,\n ...options\n });\n }\n}\n\nconst DANGER_KINDS = [\n 'invalid_password', 'totp:login_failed', 'totp:confirm_failed',\n 'passkey:login_failed', 'sessions:revoke_failed',\n 'email_change:invalid', 'email_change:expired'\n];\nconst SUCCESS_KINDS = [\n 'login', 'oauth', 'email_verify:confirmed', 'email_verify:confirmed_code',\n 'phone_verify:confirmed', 'phone_change:confirmed', 'username:changed'\n];\n\nclass SecurityEventRow extends TableRow {\n get kindBadgeClass() {\n const kind = this.model?.get('kind') || '';\n if (DANGER_KINDS.includes(kind)) return 'bg-danger';\n if (SUCCESS_KINDS.includes(kind)) return 'bg-success';\n return 'bg-secondary';\n }\n\n get kindLabel() {\n const kind = this.model?.get('kind') || '';\n return kind.replace(/[_:]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase());\n }\n\n get kindIcon() {\n const kind = this.model?.get('kind') || '';\n if (DANGER_KINDS.includes(kind)) return 'bi-exclamation-triangle';\n if (SUCCESS_KINDS.includes(kind)) return 'bi-check-circle';\n return 'bi-info-circle';\n }\n}\n\nexport default class ProfileSecurityEventsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-security-events-section',\n template: `<div id=\"security-events-table\"></div>`,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.tableView = new TableView({\n containerId: 'security-events-table',\n collection: new SecurityEventList(),\n defaultQuery: { sort: '-created' },\n hideActivePillNames: ['sort'],\n itemClass: SecurityEventRow,\n columns: [\n {\n key: 'kind',\n label: 'Event',\n template: '<span class=\"badge {{kindBadgeClass}}\"><i class=\"bi {{kindIcon}} me-1\"></i>{{kindLabel}}</span>'\n },\n {\n key: 'summary',\n label: 'Details'\n },\n { key: 'ip', label: 'IP', visibility: 'lg' },\n { key: 'created|relative', label: 'Time', sortable: true }\n ],\n searchable: true,\n sortable: true,\n filterable: false,\n paginated: true,\n showAdd: false,\n showExport: false,\n tableOptions: {\n striped: false,\n hover: true,\n size: 'sm'\n },\n emptyMessage: 'No security events'\n });\n this.addChild(this.tableView);\n }\n}\n","/**\n * ProfileNotificationsSection - Notification preferences tab\n *\n * Per-kind, per-channel toggle grid. Kinds are dynamically loaded\n * from the API. Channels: in_app, email, push.\n */\nimport View from '@core/View.js';\nimport rest from '@core/Rest.js';\n\nconst CHANNEL_LABELS = {\n in_app: 'In-App',\n email: 'Email',\n push: 'Push'\n};\n\nconst CHANNELS = ['in_app', 'email', 'push'];\n\nexport default class ProfileNotificationsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-notifications-section',\n template: `\n <style>\n .pn-table { width: 100%; border-collapse: collapse; }\n .pn-table th { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; padding: 0.5rem 0.75rem; border-bottom: 2px solid #e9ecef; }\n .pn-table th:first-child { text-align: left; }\n .pn-table th:not(:first-child) { text-align: center; width: 80px; }\n .pn-table td { padding: 0.65rem 0.75rem; border-bottom: 1px solid #f0f0f0; }\n .pn-table td:first-child { font-size: 0.88rem; font-weight: 500; text-transform: capitalize; }\n .pn-table td:not(:first-child) { text-align: center; }\n .pn-table tr:last-child td { border-bottom: none; }\n .pn-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pn-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n\n {{#hasPreferences|bool}}\n <table class=\"pn-table\">\n <thead>\n <tr>\n <th>Type</th>\n {{#channels}}\n <th>{{.label}}</th>\n {{/channels}}\n </tr>\n </thead>\n <tbody>\n {{#preferenceRows}}\n <tr>\n <td>{{.kindLabel}}</td>\n {{#.toggles}}\n <td>\n <input type=\"checkbox\" class=\"form-check-input\"\n data-action=\"toggle-pref\"\n data-kind=\"{{.kind}}\"\n data-channel=\"{{.channel}}\"\n {{#.checked}}checked{{/.checked}}>\n </td>\n {{/.toggles}}\n </tr>\n {{/preferenceRows}}\n </tbody>\n </table>\n {{/hasPreferences|bool}}\n {{^hasPreferences|bool}}\n <div class=\"pn-empty\">\n <i class=\"bi bi-bell\"></i>\n No notification preferences configured\n <div style=\"font-size: 0.78rem; margin-top: 0.5rem;\">\n Preferences will appear here once notification types are defined.\n </div>\n </div>\n {{/hasPreferences|bool}}\n `,\n ...options\n });\n this.preferences = {};\n }\n\n get channels() {\n return CHANNELS.map(ch => ({ key: ch, label: CHANNEL_LABELS[ch] || ch }));\n }\n\n get hasPreferences() {\n return Object.keys(this.preferences).length > 0;\n }\n\n get preferenceRows() {\n return Object.keys(this.preferences).sort().map(kind => ({\n kind,\n kindLabel: kind.replace(/[_-]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),\n toggles: CHANNELS.map(channel => ({\n kind,\n channel,\n checked: this.preferences[kind]?.[channel] !== false\n }))\n }));\n }\n\n async onBeforeRender() {\n try {\n const resp = await rest.GET('/api/account/notification/preferences', {}, { dataOnly: true });\n this.preferences = resp?.data?.preferences || resp?.data || {};\n } catch (e) {\n this.preferences = {};\n }\n }\n\n async onActionTogglePref(event, el) {\n const kind = el.dataset.kind;\n const channel = el.dataset.channel;\n const checked = el.checked;\n\n // Update local state\n if (!this.preferences[kind]) {\n this.preferences[kind] = {};\n }\n this.preferences[kind][channel] = checked;\n\n // Save to API\n try {\n const resp = await rest.POST('/api/account/notification/preferences', {\n preferences: { [kind]: { [channel]: checked } }\n });\n if (!resp.success) {\n this.getApp()?.toast?.error(resp.message || 'Failed to update preference');\n el.checked = !checked; // revert\n }\n } catch (e) {\n this.getApp()?.toast?.error('Failed to update preference');\n el.checked = !checked; // revert\n }\n return true;\n }\n}\n","/**\n * ProfileApiKeysSection - Personal API key generation\n *\n * Generate IP-restricted long-lived JWT for programmatic access.\n * Token is shown only once after generation.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport rest from '@core/Rest.js';\n\nexport default class ProfileApiKeysSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-api-keys-section',\n template: `\n <style>\n .pak-warning { padding: 0.85rem 1rem; background: #fff3cd; border: 1px solid #ffecb5; border-radius: 8px; margin-bottom: 1.25rem; font-size: 0.82rem; color: #664d03; display: flex; align-items: flex-start; gap: 0.6rem; }\n .pak-warning i { font-size: 1rem; flex-shrink: 0; margin-top: 0.1rem; }\n .pak-form-label { font-size: 0.8rem; font-weight: 600; color: #495057; margin-bottom: 0.35rem; }\n .pak-help { font-size: 0.73rem; color: #6c757d; margin-top: 0.25rem; }\n .pak-field { margin-bottom: 1rem; }\n .pak-generate { margin-top: 0.5rem; }\n .pak-result { padding: 1rem; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; margin-top: 1.25rem; }\n .pak-result-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #0f5132; margin-bottom: 0.5rem; }\n .pak-token-wrap { display: flex; gap: 0.5rem; align-items: center; }\n .pak-token { flex: 1; font-family: monospace; font-size: 0.78rem; padding: 0.5rem 0.75rem; background: #fff; border: 1px solid #dee2e6; border-radius: 4px; word-break: break-all; max-height: 80px; overflow-y: auto; }\n .pak-token-warning { font-size: 0.75rem; color: #dc3545; margin-top: 0.5rem; font-weight: 600; }\n </style>\n\n <div class=\"pak-warning\">\n <i class=\"bi bi-exclamation-triangle-fill\"></i>\n <div>\n <strong>Treat API keys like passwords.</strong> They carry your full account permissions.\n Store them securely and never expose them in client-side code.\n </div>\n </div>\n\n <div class=\"pak-field\">\n <div class=\"pak-form-label\">Allowed IPs</div>\n <input type=\"text\" id=\"pak-allowed-ips\" class=\"form-control form-control-sm\"\n placeholder=\"e.g., 203.0.113.0/24, 10.0.0.1\" />\n <div class=\"pak-help\">Optional. Comma-separated IP addresses or CIDR ranges to restrict access.</div>\n </div>\n\n <div class=\"pak-field\">\n <div class=\"pak-form-label\">Expiration</div>\n <select id=\"pak-expire-days\" class=\"form-select form-select-sm\" style=\"max-width: 200px;\">\n <option value=\"30\">30 days</option>\n <option value=\"60\">60 days</option>\n <option value=\"90\" selected>90 days</option>\n <option value=\"180\">180 days</option>\n <option value=\"360\">360 days</option>\n </select>\n </div>\n\n <button type=\"button\" class=\"btn btn-primary btn-sm pak-generate\" data-action=\"generate-key\">\n <i class=\"bi bi-key me-1\"></i>Generate API Key\n </button>\n\n <div id=\"pak-result\" style=\"display: none;\">\n <div class=\"pak-result\">\n <div class=\"pak-result-label\">Your API Key</div>\n <div class=\"pak-token-wrap\">\n <div class=\"pak-token\" id=\"pak-token-display\"></div>\n <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" data-action=\"copy-token\" title=\"Copy\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n <div class=\"pak-token-warning\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>This token will not be shown again. Copy it now.\n </div>\n </div>\n </div>\n `,\n ...options\n });\n this.generatedToken = null;\n }\n\n async onActionGenerateKey() {\n const ipsInput = this.element.querySelector('#pak-allowed-ips')?.value?.trim();\n const allowed_ips = ipsInput\n ? ipsInput.split(',').map(ip => ip.trim()).filter(Boolean)\n : [];\n\n const expireDays = parseInt(this.element.querySelector('#pak-expire-days')?.value || '90', 10);\n\n const confirmed = await Dialog.confirm(\n 'Generate a new API key? This key will have full access to your account.' +\n (allowed_ips.length ? '' : ' No IP restrictions will be applied.'),\n 'Generate API Key'\n );\n if (!confirmed) return true;\n\n const body = { expire_days: expireDays };\n if (allowed_ips.length) body.allowed_ips = allowed_ips;\n\n const resp = await rest.POST('/api/auth/generate_api_key', body, {}, { dataOnly: true });\n\n if (resp.success && resp.data?.token) {\n this.generatedToken = resp.data.token;\n const resultEl = this.element.querySelector('#pak-result');\n const tokenEl = this.element.querySelector('#pak-token-display');\n if (resultEl && tokenEl) {\n tokenEl.textContent = this.generatedToken;\n resultEl.style.display = 'block';\n }\n this.getApp()?.toast?.success('API key generated');\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to generate API key');\n }\n return true;\n }\n\n async onActionCopyToken() {\n if (this.generatedToken) {\n try {\n await navigator.clipboard.writeText(this.generatedToken);\n this.getApp()?.toast?.success('Token copied to clipboard');\n } catch {\n this.getApp()?.toast?.error('Failed to copy token');\n }\n }\n return true;\n }\n}\n","/**\n * ProfileGroupsSection - Groups/memberships tab\n *\n * Uses ListView + MemberList to show user's group\n * memberships with role badges.\n */\nimport View from '@core/View.js';\nimport ListView from '@core/views/list/ListView.js';\nimport ListViewItem from '@core/views/list/ListViewItem.js';\nimport { MemberList } from '@core/models/Member.js';\n\nconst AVATAR_COLORS = ['#667eea', '#f5576c', '#38b2ac', '#ed8936', '#9f7aea', '#48bb78', '#4299e1', '#ed64a6'];\n\nclass GroupMemberItem extends ListViewItem {\n get groupName() {\n return this.model?.get('group')?.name || 'Unknown Group';\n }\n\n get groupKind() {\n const kind = this.model?.get('group')?.kind || '';\n return kind.replace(/^\\w/, c => c.toUpperCase());\n }\n\n get initials() {\n return this.groupName.split(/\\s+/).map(w => w[0]).join('').substring(0, 2).toUpperCase();\n }\n\n get avatarColor() {\n const name = this.groupName;\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];\n }\n\n get roleName() {\n let role = this.model?.get('metadata')?.role || '';\n if (!role && this.model?.get('permissions')?.manage_group) {\n role = 'Admin';\n }\n return role;\n }\n\n get hasRole() {\n return !!this.roleName;\n }\n\n get roleBadgeClass() {\n const r = (this.roleName || '').toLowerCase();\n if (r === 'owner') return 'bg-primary';\n if (r === 'admin') return 'bg-info';\n return 'bg-secondary';\n }\n}\n\nexport default class ProfileGroupsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-groups-section',\n template: `\n <style>\n .pg-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0; border-bottom: 1px solid #f0f0f0; }\n .pg-row:last-child { border-bottom: none; }\n .pg-avatar { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 700; color: #fff; flex-shrink: 0; }\n .pg-info { flex: 1; }\n .pg-name { font-weight: 600; font-size: 0.88rem; }\n .pg-meta { font-size: 0.73rem; color: #6c757d; }\n .pg-role { font-size: 0.7rem; }\n </style>\n <div id=\"groups-list\"></div>\n `,\n ...options\n });\n }\n\n async onInit() {\n await super.onInit();\n this.listView = new ListView({\n containerId: 'groups-list',\n collection: new MemberList({ size: 50 }),\n defaultQuery: { user: this.model.id },\n itemClass: GroupMemberItem,\n itemTemplate: `\n <div class=\"pg-row\">\n <div class=\"pg-avatar\" style=\"background: {{avatarColor}};\">{{initials}}</div>\n <div class=\"pg-info\">\n <div class=\"pg-name\">{{groupName}}</div>\n <div class=\"pg-meta\">{{groupKind}}</div>\n </div>\n {{#hasRole|bool}}\n <span class=\"pg-role badge {{roleBadgeClass}}\">{{roleName}}</span>\n {{/hasRole|bool}}\n </div>\n `,\n emptyMessage: 'You are not a member of any groups'\n });\n this.addChild(this.listView);\n }\n}\n","/**\n * ProfilePermissionsSection - Read-only permissions tab\n *\n * Shows role indicator and flat permission tag cloud.\n * Pure template view — no fetching needed.\n */\nimport View from '@core/View.js';\nimport { User } from '@core/models/User.js';\n\nexport default class ProfilePermissionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'profile-permissions-section',\n template: `\n <style>\n .pp-role-bar { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 1rem; background: #f0f0ff; border-radius: 8px; margin-bottom: 1.25rem; font-size: 0.85rem; }\n .pp-role-bar i { color: #6f42c1; font-size: 1rem; }\n .pp-role-bar strong { color: #6f42c1; }\n .pp-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; }\n .pp-grid { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 1.25rem; }\n .pp-tag { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem; padding: 0.25em 0.6em; background: #f8f9fc; border: 1px solid #e9ecef; border-radius: 4px; color: #495057; }\n .pp-tag i { font-size: 0.65rem; color: #198754; }\n .pp-note { font-size: 0.78rem; color: #adb5bd; margin-top: 1rem; }\n .pp-empty { color: #6c757d; font-style: italic; font-size: 0.85rem; padding: 1rem 0; }\n </style>\n\n {{#model.is_superuser|bool}}\n <div class=\"pp-role-bar\">\n <i class=\"bi bi-star-fill\"></i>\n <span><strong>Superuser</strong> — full system access</span>\n </div>\n {{/model.is_superuser|bool}}\n\n <div class=\"pp-section-label\">Granted Permissions</div>\n\n {{#permissionTags|bool}}\n <div class=\"pp-grid\">\n {{#permissionTags}}\n <span class=\"pp-tag\"><i class=\"bi bi-check-circle-fill\"></i> {{.}}</span>\n {{/permissionTags}}\n </div>\n {{/permissionTags|bool}}\n\n {{^permissionTags|bool}}\n <div class=\"pp-empty\">No permissions assigned</div>\n {{/permissionTags|bool}}\n\n <div class=\"pp-note\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Permissions are managed by your administrator.\n </div>\n `,\n ...options\n });\n }\n\n get permissionTags() {\n if (!this.model) return [];\n const perms = this.model.get('permissions');\n if (!perms) return [];\n\n const permMap = {};\n User.PERMISSIONS.forEach(p => { permMap[p.name] = p.label; });\n\n return Object.keys(perms)\n .filter(k => perms[k] === true)\n .map(k => permMap[k] || k.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()));\n }\n}\n","/**\n * UserProfileView - Rich user profile shown in a Dialog\n *\n * Main container with left nav and section switching.\n * 11 sections: Profile, Personal, Security, Connected, Sessions, Devices,\n * Security Events, Notifications, API Keys, Groups, Permissions.\n * Fetches full user data on render.\n * Thin accent bar + compact header with avatar, name, and status badges.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport { File as FileModel } from '@core/models/Files.js';\nimport ProfileOverviewSection from './ProfileOverviewSection.js';\nimport ProfilePersonalSection from './ProfilePersonalSection.js';\nimport ProfileSecuritySection from './ProfileSecuritySection.js';\nimport ProfileConnectedSection from './ProfileConnectedSection.js';\nimport ProfileSessionsSection from './ProfileSessionsSection.js';\nimport ProfileDevicesSection from './ProfileDevicesSection.js';\nimport ProfileSecurityEventsSection from './ProfileSecurityEventsSection.js';\nimport ProfileNotificationsSection from './ProfileNotificationsSection.js';\nimport ProfileApiKeysSection from './ProfileApiKeysSection.js';\nimport ProfileGroupsSection from './ProfileGroupsSection.js';\nimport ProfilePermissionsSection from './ProfilePermissionsSection.js';\n\nconst SECTIONS = {\n profile: ProfileOverviewSection,\n personal: ProfilePersonalSection,\n security: ProfileSecuritySection,\n connected: ProfileConnectedSection,\n sessions: ProfileSessionsSection,\n devices: ProfileDevicesSection,\n security_events: ProfileSecurityEventsSection,\n notifications: ProfileNotificationsSection,\n api_keys: ProfileApiKeysSection,\n groups: ProfileGroupsSection,\n permissions: ProfilePermissionsSection\n};\n\nexport default class UserProfileView extends View {\n constructor(options = {}) {\n super({\n className: 'user-profile-view',\n template: `\n <style>\n .up-layout { display: flex; height: 100%; }\n .up-nav { width: 200px; background: #f8f9fc; border-right: 1px solid #e9ecef; padding: 0.75rem 0; flex-shrink: 0; overflow-y: auto; }\n .up-nav-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; padding: 0.75rem 1.25rem 0.25rem; }\n .up-nav a { color: #495057; padding: 0.45rem 1.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; text-decoration: none; }\n .up-nav a:hover { background: #e9ecef; }\n .up-nav a.active { background: #e7f1ff; color: #0d6efd; font-weight: 600; border-right: 2px solid #0d6efd; }\n .up-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .up-content { flex: 1; overflow-y: auto; padding: 1.5rem 2.5rem; }\n .up-accent { height: 4px; background: linear-gradient(90deg, #1a73e8, #4fc3f7); border-radius: var(--bs-modal-border-radius, 0.5rem) var(--bs-modal-border-radius, 0.5rem) 0 0; }\n .up-header { display: flex; align-items: center; gap: 1rem; padding: 1rem 1.5rem; border-bottom: 1px solid #e9ecef; }\n .up-avatar-wrap { position: relative; flex-shrink: 0; cursor: pointer; }\n .up-avatar-wrap img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; }\n .up-avatar-initials { width: 56px; height: 56px; border-radius: 50%; background: #e7f1ff; color: #0d6efd; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 700; }\n .up-header-info { flex: 1; min-width: 0; }\n .up-header-name { display: flex; align-items: center; gap: 0.5rem; }\n .up-header-name h5 { margin: 0; font-weight: 700; font-size: 1.05rem; }\n .up-header-badge { font-size: 0.65rem; padding: 0.15em 0.5em; border-radius: 3px; font-weight: 600; }\n .up-header-badge-staff { background: #e7f1ff; color: #0d6efd; }\n .up-header-badge-su { background: #fff3cd; color: #856404; }\n .up-header-sub { font-size: 0.78rem; color: #6c757d; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }\n .up-header-sub .up-dot { color: #ced4da; }\n .up-header-verified { display: inline-flex; align-items: center; gap: 0.2rem; font-size: 0.72rem; color: #198754; }\n .up-header-verified i { font-size: 0.7rem; }\n .up-close { flex-shrink: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border: none; background: none; color: #6c757d; font-size: 1.1rem; border-radius: 6px; cursor: pointer; padding: 0; align-self: flex-start; margin-top: -0.15rem; }\n .up-close:hover { background: #f0f0f0; color: #212529; }\n @media (max-width: 576px) {\n .up-nav { display: none; }\n .up-content { padding: 1.25rem; }\n }\n </style>\n <div class=\"up-layout\" style=\"flex-direction: column; min-height: 480px;\">\n <div class=\"up-accent\"></div>\n <div class=\"up-header\">\n <div class=\"up-avatar-wrap\" data-action=\"change-avatar\" title=\"Change avatar\">\n {{{model.avatar|avatar}}}\n </div>\n <div class=\"up-header-info\">\n <div class=\"up-header-name\">\n <h5>{{model.display_name}}</h5>\n {{#model.is_superuser|bool}}<span class=\"up-header-badge up-header-badge-su\">Superuser</span>{{/model.is_superuser|bool}}\n {{^model.is_superuser|bool}}\n {{#model.is_staff|bool}}<span class=\"up-header-badge up-header-badge-staff\">Staff</span>{{/model.is_staff|bool}}\n {{/model.is_superuser|bool}}\n </div>\n <div class=\"up-header-sub\">\n <span>{{model.email}}</span>\n {{#model.is_email_verified|bool}}\n <span class=\"up-dot\">·</span>\n <span class=\"up-header-verified\"><i class=\"bi bi-patch-check-fill\"></i> Email</span>\n {{/model.is_email_verified|bool}}\n {{#model.requires_mfa|bool}}\n <span class=\"up-dot\">·</span>\n <span class=\"up-header-verified\"><i class=\"bi bi-shield-fill-check\"></i> MFA</span>\n {{/model.requires_mfa|bool}}\n </div>\n </div>\n <button type=\"button\" class=\"up-close\" data-action=\"close-dialog\" title=\"Close\"><i class=\"bi bi-x-lg\"></i></button>\n </div>\n <div class=\"up-layout\" style=\"flex: 1; min-height: 0;\">\n <nav class=\"up-nav\">\n <a href=\"#\" class=\"nav-link active\" data-action=\"navigate\" data-section=\"profile\"><i class=\"bi bi-person\"></i> Profile</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"personal\"><i class=\"bi bi-person-vcard\"></i> Personal</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"security\"><i class=\"bi bi-shield-lock\"></i> Security</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"connected\"><i class=\"bi bi-plug\"></i> Connected</a>\n <div class=\"up-nav-label\">Activity</div>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"sessions\"><i class=\"bi bi-clock-history\"></i> Sessions</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"devices\"><i class=\"bi bi-laptop\"></i> Devices</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"security_events\"><i class=\"bi bi-shield-exclamation\"></i> Security Events</a>\n <div class=\"up-nav-label\">Settings</div>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"notifications\"><i class=\"bi bi-bell\"></i> Notifications</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"api_keys\"><i class=\"bi bi-key\"></i> API Keys</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"groups\"><i class=\"bi bi-people\"></i> Groups</a>\n <a href=\"#\" class=\"nav-link\" data-action=\"navigate\" data-section=\"permissions\"><i class=\"bi bi-shield-check\"></i> Permissions</a>\n </nav>\n <div class=\"up-content\" id=\"profile-section\"></div>\n </div>\n </div>\n `,\n ...options\n });\n this.activeSection = 'profile';\n this.sectionView = null;\n }\n\n get hasAvatar() {\n return !!(this.model && this.model.get('avatar') && this.model.get('avatar').url);\n }\n\n async onBeforeRender() {\n if (this.model) {\n await this.model.fetch({ params: { graph: 'full' } });\n }\n }\n\n async onAfterRender() {\n // Sync nav active state after any re-render (e.g., model change auto-rerender)\n this.element?.querySelectorAll('.up-nav a').forEach(link => {\n link.classList.toggle('active', link.dataset.section === this.activeSection);\n });\n }\n\n async onInit() {\n await super.onInit();\n\n // Default section\n this.sectionView = new ProfileOverviewSection({\n model: this.model,\n containerId: 'profile-section'\n });\n this.addChild(this.sectionView);\n }\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (!section || section === this.activeSection) return true;\n\n const SectionClass = SECTIONS[section];\n if (!SectionClass) return true;\n\n // Remove current section\n if (this.sectionView) {\n this.removeChild(this.sectionView);\n }\n\n // Create and add new section\n this.sectionView = new SectionClass({\n model: this.model,\n containerId: 'profile-section'\n });\n this.addChild(this.sectionView);\n await this.sectionView.render();\n\n this.activeSection = section;\n\n // Update nav active state\n this.element.querySelectorAll('.up-nav a').forEach(link => {\n link.classList.toggle('active', link.dataset.section === section);\n });\n\n return true;\n }\n\n async onActionChangeAvatar() {\n const resp = await Dialog.updateModelImage({\n model: this.model,\n field: 'avatar',\n title: 'Change Avatar',\n upload: true,\n }, {\n name: 'avatar',\n size: 'lg',\n imageSize: { width: 200, height: 200 },\n placeholder: 'Upload your avatar',\n });\n if (resp && resp.status === 200) {\n await this.render();\n }\n }\n\n async onActionCloseDialog() {\n // Find and dismiss the parent dialog\n const modal = this.element?.closest('.modal');\n if (modal) {\n const bsModal = bootstrap?.Modal?.getInstance(modal);\n if (bsModal) bsModal.hide();\n }\n return true;\n }\n\n async onActionChangePassword() {\n const app = this.getApp();\n if (app && app.changePassword) {\n await app.changePassword();\n }\n return true;\n }\n\n async onActionUpdateEmail() {\n const app = this.getApp();\n const rest = app.rest;\n\n // Step 1: Collect new email\n const data = await Dialog.showForm({\n title: 'Change Email Address',\n size: 'sm',\n submitText: 'Send Code',\n fields: [\n {\n name: 'new_email_address',\n type: 'text',\n label: 'New Email Address',\n required: true,\n placeholder: 'Enter new email address',\n attributes: { autocomplete: 'off', inputmode: 'email' },\n cols: 12\n }\n ]\n });\n if (!data) return true;\n\n // Step 2: Request the change (sends code to new email)\n const sendResp = await rest.POST('/api/auth/email/change/request', {\n email: data.new_email_address,\n method: 'code'\n });\n if (!sendResp.success) {\n app.toast.error(sendResp.message || 'Failed to request email change');\n return true;\n }\n\n // Step 3: Prompt for the 6-digit code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${data.new_email_address}</strong>`,\n 'Confirm Email Change',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 4: Confirm the code — returns new JWT\n const confirmResp = await rest.POST('/api/auth/email/change/confirm', { code: code.trim() }, {}, { dataOnly: true });\n if (confirmResp.success && confirmResp.data) {\n // Store new tokens (old sessions are invalidated)\n if (confirmResp.data.access_token) {\n app.auth?.setTokens?.(confirmResp.data);\n }\n app.toast.success('Email address updated');\n await this.model.fetch({ params: { graph: 'full' } });\n await this.render();\n } else {\n app.toast.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n\n async onActionUpdatePhone() {\n const app = this.getApp();\n const rest = app.rest;\n const currentPhone = this.model.get('phone_number');\n\n if (!currentPhone) {\n // No phone — redirect to add phone flow in overview section\n app.toast.info('Use the profile section to add a phone number');\n return true;\n }\n\n // Step 1: Collect new phone + password\n const data = await Dialog.showForm({\n title: 'Change Phone Number',\n size: 'sm',\n submitText: 'Send Code',\n fields: [\n {\n name: 'new_phone',\n type: 'tel',\n label: 'New Phone Number',\n required: true,\n placeholder: '+14155550123',\n attributes: { autocomplete: 'off' },\n cols: 12\n }\n ]\n });\n if (!data) return true;\n\n // Step 2: Request the change (sends code to new phone)\n const sendResp = await rest.POST('/api/auth/phone/change/request', {\n phone_number: data.new_phone\n }, {}, { dataOnly: true });\n if (!sendResp.success) {\n app.toast.error(sendResp.message || 'Failed to request phone change');\n return true;\n }\n\n // Hold the session_token for confirm step\n const sessionToken = sendResp.data?.session_token;\n\n // Step 3: Prompt for the 6-digit code\n const code = await Dialog.prompt(\n `Enter the 6-digit code sent to <strong>${data.new_phone}</strong>`,\n 'Confirm Phone Change',\n { placeholder: '000000' }\n );\n if (!code) return true;\n\n // Step 4: Confirm the code\n const confirmResp = await rest.POST('/api/auth/phone/change/confirm', {\n session_token: sessionToken,\n code: code.trim()\n });\n if (confirmResp.success) {\n app.toast.success('Phone number updated');\n await this.model.fetch({ params: { graph: 'full' } });\n await this.render();\n } else {\n app.toast.error(confirmResp.message || 'Invalid or expired code');\n }\n return true;\n }\n}\n","/**\n * PasskeySetupView - Post-login passkey setup prompt\n *\n * Compact centered dialog body shown after login to encourage\n * users to register a passkey. Create / Skip / Don't ask again.\n */\nimport View from '@core/View.js';\nimport Dialog from '@core/views/feedback/Dialog.js';\nimport { Passkey } from '@core/models/Passkeys.js';\n\nexport default class PasskeySetupView extends View {\n constructor(options = {}) {\n super({\n className: 'passkey-setup-view',\n template: `\n <style>\n .pks-body { padding: 2rem 1.75rem 1rem; text-align: center; }\n .pks-icon { width: 56px; height: 56px; background: #e7f1ff; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 1.5rem; color: #0d6efd; margin-bottom: 1rem; }\n .pks-body h5 { font-weight: 700; font-size: 1.05rem; margin-bottom: 0.35rem; }\n .pks-body p { font-size: 0.83rem; color: #6c757d; margin-bottom: 1.25rem; line-height: 1.45; }\n .pks-footer { padding: 0 1.75rem 1.5rem; display: flex; flex-direction: column; gap: 0.4rem; }\n .pks-footer .btn-create { padding: 0.6rem; font-weight: 600; font-size: 0.9rem; border-radius: 8px; }\n .pks-footer .btn-skip { background: none; border: none; color: #6c757d; font-size: 0.82rem; padding: 0.4rem; cursor: pointer; }\n .pks-footer .btn-skip:hover { color: #495057; }\n .pks-dont-show { text-align: center; padding: 0 1.75rem 1.25rem; }\n .pks-dont-show label { font-size: 0.73rem; color: #adb5bd; cursor: pointer; }\n </style>\n\n <div class=\"pks-body\">\n <div class=\"pks-icon\"><i class=\"bi bi-fingerprint\"></i></div>\n <h5>Add a Passkey</h5>\n <p>Sign in faster with Face ID, Touch ID, or your device PIN. No passwords needed.</p>\n </div>\n <div class=\"pks-footer\">\n <button class=\"btn btn-primary btn-create\" data-action=\"create-passkey\"><i class=\"bi bi-fingerprint me-1\"></i>Create Passkey</button>\n <button class=\"btn-skip\" data-action=\"skip\">Not now</button>\n </div>\n <div class=\"pks-dont-show\">\n <label><input type=\"checkbox\" class=\"form-check-input form-check-input-sm me-1\" data-action=\"dont-ask\"> Don't ask again</label>\n </div>\n `,\n ...options\n });\n }\n\n _base64urlToBytes(base64url) {\n const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);\n return Uint8Array.from(atob(padded), c => c.charCodeAt(0));\n }\n\n async onActionCreatePasskey() {\n try {\n const beginResp = await Passkey.registerBegin();\n if (!beginResp.success || !beginResp.data) {\n this.getApp()?.toast?.error('Failed to start passkey registration');\n return true;\n }\n\n const options = beginResp.data.data || beginResp.data;\n const publicKey = options.publicKey;\n\n // Override rp.id to match current domain (server may return production domain)\n if (publicKey.rp) {\n publicKey.rp.id = window.location.hostname;\n }\n\n // Decode challenge and user.id from base64url\n if (publicKey.challenge && typeof publicKey.challenge === 'string') {\n publicKey.challenge = this._base64urlToBytes(publicKey.challenge);\n }\n if (publicKey.user && publicKey.user.id && typeof publicKey.user.id === 'string') {\n publicKey.user.id = this._base64urlToBytes(publicKey.user.id);\n }\n if (publicKey.excludeCredentials) {\n publicKey.excludeCredentials = publicKey.excludeCredentials.map(cred => ({\n ...cred,\n id: typeof cred.id === 'string' ? this._base64urlToBytes(cred.id) : cred.id\n }));\n }\n\n const credential = await navigator.credentials.create({ publicKey });\n if (!credential) {\n this.getApp()?.toast?.error('Passkey creation was cancelled');\n return true;\n }\n\n // Prompt for friendly name\n const friendlyName = await Dialog.prompt('Name this passkey:', 'Passkey Name', {\n defaultValue: '',\n placeholder: 'e.g., My MacBook'\n });\n\n const credentialData = {\n id: credential.id,\n rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),\n type: credential.type,\n response: {\n clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),\n attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject)))\n }\n };\n\n if (credential.response.getTransports) {\n credentialData.transports = credential.response.getTransports();\n }\n\n const completeResp = await Passkey.registerComplete({\n challenge_id: options.challenge_id,\n credential: credentialData,\n friendly_name: friendlyName || 'My Passkey'\n });\n\n if (completeResp.success) {\n this.getApp()?.toast?.success('Passkey registered successfully');\n // Dismiss after successful registration\n localStorage.setItem('passkey_setup_dismissed', '1');\n this.emit('dismiss');\n } else {\n this.getApp()?.toast?.error(completeResp.error || 'Failed to register passkey');\n }\n } catch (err) {\n if (err.name === 'NotAllowedError') return true;\n if (err.name === 'SecurityError') {\n this.getApp()?.toast?.error('Passkeys are not supported on this domain');\n } else {\n console.error('Passkey registration error:', err);\n this.getApp()?.toast?.error('Passkey registration failed');\n }\n }\n return true;\n }\n\n async onActionSkip() {\n this.emit('dismiss');\n return true;\n }\n\n async onActionDontAsk() {\n const checkbox = this.element?.querySelector('.pks-dont-show input[type=\"checkbox\"]');\n if (checkbox && checkbox.checked) {\n localStorage.setItem('passkey_setup_dismissed', '1');\n this.emit('dismiss');\n } else {\n localStorage.removeItem('passkey_setup_dismissed');\n }\n return true;\n }\n}\n","/**\n * MOJO Framework - Core Entry (2.1.0)\n */\n\n// Bundle core CSS\nimport '@core/css/core.css';\nimport '@core/css/portal.css';\nimport '@core/css/table.css';\nimport '@core/css/toast.css';\nimport '@core/css/chat.css';\n\nimport ConsoleSilencer from '@core/utils/ConsoleSilencer.js';\n// Reduce console noise globally: errors only by default (suppress logs, info, and warnings)\nConsoleSilencer.install({ level: 'warn' });\n\n// Version info\nexport {\n VERSION_INFO,\n VERSION,\n VERSION_MAJOR,\n VERSION_MINOR,\n VERSION_REVISION,\n BUILD_TIME\n} from './version.js';\n\n// Core runtime\nexport { default as View } from '@core/View.js';\nexport { default as Page } from '@core/Page.js';\nexport { default as Router } from '@core/Router.js';\nexport { default as Model } from '@core/Model.js';\nexport { default as Collection } from '@core/Collection.js';\nexport { default as Rest } from '@core/Rest.js';\n\n// Core Models - re-export everything from models/\nexport * from '@core/models/AWS.js';\nexport * from '@core/models/Email.js';\nexport * from '@core/models/Files.js';\nexport * from '@core/models/Group.js';\nexport * from '@core/models/Incident.js';\nexport * from '@core/models/Job.js';\nexport * from '@core/models/JobRunner.js';\nexport * from '@core/models/Log.js';\nexport * from '@core/models/Member.js';\nexport * from '@core/models/Metrics.js';\nexport * from '@core/models/Push.js';\nexport * from '@core/models/System.js';\nexport * from '@core/models/Tickets.js';\nexport * from '@core/models/User.js';\n\n// App classes\nexport { default as WebApp } from '@core/WebApp.js';\nexport { default as PortalApp } from '@core/PortalApp.js';\n\n// UI helper\nexport { default as Dialog } from '@core/views/feedback/Dialog.js';\n\n// Selected views (curated for tree-shaking)\nexport { default as TableView } from '@core/views/table/TableView.js';\nexport { default as TableRow } from '@core/views/table/TableRow.js';\nexport { default as TablePage } from '@core/pages/TablePage.js';\nexport { default as ListView } from '@core/views/list/ListView.js';\nexport { default as ListViewItem } from '@core/views/list/ListViewItem.js';\nexport { default as TopNav } from '@core/views/navigation/TopNav.js';\nexport { default as Sidebar } from '@core/views/navigation/Sidebar.js';\nexport { default as TabView } from '@core/views/navigation/TabView.js';\nexport { default as SimpleSearchView } from '@core/views/navigation/SimpleSearchView.js';\nexport { default as DataView } from '@core/views/data/DataView.js';\nexport { default as FormView } from '@core/forms/FormView.js';\nexport { default as FormPage } from '@core/forms/FormPage.js';\nexport { default as FilePreviewView } from '@core/views/data/FilePreviewView.js';\nexport { default as ChatView } from '@core/views/chat/ChatView.js';\nexport { default as ChatMessageView } from '@core/views/chat/ChatMessageView.js';\nexport { default as ChatInputView } from '@core/views/chat/ChatInputView.js';\n\n// Services, utils, mixins\nexport { default as FileUpload } from '@core/services/FileUpload.js';\nexport { default as applyFileDropMixin } from '@core/mixins/FileDropMixin.js';\nexport { default as TokenManager } from '@core/services/TokenManager.js';\nexport { default as ToastService } from '@core/services/ToastService.js';\nexport { default as WebSocketClient } from '@core/services/WebSocketClient.js';\nexport { default as EventDelegate } from '@core/mixins/EventDelegate.js';\nexport { default as EventBus } from '@core/utils/EventBus.js';\nexport { default as dataFormatter } from '@core/utils/DataFormatter.js';\nexport { default as MustacheFormatter } from '@core/utils/MustacheFormatter.js';\nexport { default as MOJOUtils, DataWrapper } from '@core/utils/MOJOUtils.js';\nexport { default as ConsoleSilencer } from '@core/utils/ConsoleSilencer.js';\nexport { installConsoleSilencer } from '@core/utils/ConsoleSilencer.js';\nexport { default as DjangoLookups, parseFilterKey, formatFilterDisplay, LOOKUPS } from '@core/utils/DjangoLookups.js';\n\n// Additional views\nexport { default as ProgressView } from '@core/views/feedback/ProgressView.js';\nexport { default as ContextMenu } from '@core/views/feedback/ContextMenu.js';\n\n// User profile views\nexport { UserProfileView, PasskeySetupView } from '@core/views/user/index.js';\n\n// Names\nexport const FRAMEWORK_NAME = 'MOJO';\nexport const PACKAGE_NAME = 'web-mojo';\n\nexport default {\n FRAMEWORK_NAME,\n PACKAGE_NAME,\n};\n"],"names":["LEVELS","Object","freeze","silent","error","warn","info","log","debug","trace","all","isDev","url","document","require","pathToFileURL","__filename","href","_documentCurrentScript","tagName","toUpperCase","src","URL","baseURI","__vite_import_meta_env__","globalThis","__DEV__","process","env","NODE_ENV","isBrowser","window","GLOBAL","global","ORIGINAL_CONSOLE","console","ORIGINALS","INSTALLED","CURRENT_LEVEL","parseLevel","level","min","max","Math","key","toLowerCase","prototype","hasOwnProperty","call","makeWrapper","methodName","methodLevel","original","args","apply","ConsoleSilencer","install","options","this","setLevel","persist","explicitLevel","explicit","urlLevel","location","search","params","URLSearchParams","keys","k","v","get","parsed","getUrlLogLevel","storedLevel","localStorage","getItem","getStoredLogLevel","determineInitialLevel","patched","methodLevels","dir","table","group","groupCollapsed","groupEnd","time","timeEnd","timeLog","name","assert","condition","makeAssertWrapper","buildPatchedConsole","MOJOConsoleSilencer","uninstall","levelNumberOrName","entries","find","num","setItem","removeItem","storeLogLevel","getLevel","getLevelName","entry","criticalOnly","errorsOnly","verbose","allowAll","withTemporaryLevel","fn","prev","GroupSearchView","SimpleSearchView","constructor","super","className","trim","showKind","parentField","kindField","treeData","flattenedItems","showLines","buildTreeHierarchy","items","length","itemsById","Map","forEach","item","has","id","set","children","hasChildren","parentObj","rootItems","treeItem","itemId","originalItem","i","parentId","parent","push","calculateLevels","nodes","node","sort","a","b","localeCompare","flattenTree","result","ancestorLastFlags","index","_isLastChild","_ancestorLastFlags","allAncestorsLast","every","flag","_isLastDescendant","newFlags","computeVerticalLines","flatList","_continueVertical","s","foundSibling","j","futureItem","updateFilteredItems","collection","filteredItems","toJSON","updateResultsView","getDefaultItemTemplate","processItemTemplate","content","itemTemplate","replace","match","prop","getNestedValue","lineSegments","getRootItems","getNodeChildren","nodeId","findNode","targetId","found","Sidebar","View","menus","activeMenuName","currentRoute","showToggle","isCollapsed","sidebarTheme","theme","customView","groupHeader","groupSelectorMode","groupSelectorDialog","addClass","initializeMenus","setupRouteListeners","autoCollapseMobile","setupResponsiveBehavior","onInit","app","getApp","router","currentPath","getCurrentPath","autoSwitchToMenuForRoute","initializeTooltips","searchView","noAppend","showExitButton","headerText","containerId","Collection","GroupList","addChild","on","evt","setActiveGroup","model","hideGroupSearch","showGroupSearch","showGroupSearchDialog","setClass","showSearch","render","hide","onActionShowGroupSearch","onActionSelectGroupParent","activeGroup","Dialog","confirm","showLoading","Group","fetch","hideLoading","searchFields","searchPlaceholder","headerIcon","maxHeight","innerHeight","autoExpandRoot","autoExpandAll","indentSize","body","size","header","noBodyPadding","scrollable","buttons","closeButton","destroy","show","route","menuName","menuConfig","groupKind","menuContainsRoute","_setActiveMenu","clearAllActiveStates","setActiveItemByRoute","emit","config","sidebar","active","child","normalizeRoute","r","decoded","decodeURIComponent","startsWith","targetRoute","itemRoute","routesMatch","activeMenuItem","childRoute","doRoutesMatch","getTemplate","getSearchTemplate","getMenuTemplate","getPartials","getGroupHeader","addMenu","footer","data","getCurrentMenuConfig","setActiveMenu","lastGroupMenu","getGroupMenu","targetMenu","anyGroupMenu","_","kind","_groupKindMatches","Array","isArray","includes","showMenuForGroup","getMenuConfig","updateMenu","updates","menu","assign","removeMenu","delete","remainingMenus","from","onBeforeRender","currentMenu","subData","version","user","activeUser","renderTemplateString","processNavItems","onAfterRender","isCollapsedState","setTimeout","destroyTooltips","setCustomView","view","removeChild","clearCustomView","updateRouteWithGroup","normalizedRoute","substring","separator","map","divider","isDivider","spacer","isSpacer","processedItem","permissions","hasPermission","requiresGroupKind","isLabel","page","baseRoute","processedChild","filter","isItemActive","updateActiveItem","handleActionToggleSubmenu","event","element","arrow","querySelector","classList","toggle","handleActionToggleSidebar","toggleSidebar","onActionShowGroupMenu","action","el","onActionDefault","findAndExecuteHandler","handler","getMenuNames","hasMenu","clearMenus","clear","setMenuData","getMenuData","events","onRouteChanged","preferredMenu","sidebarMenu","fallbackMenu","portalContainer","hideAllTooltips","isCurrentlyCollapsed","contains","remove","add","setSidebarState","state","querySelectorAll","link","navText","textContent","tooltipText","setAttribute","bootstrap","Tooltip","getAttribute","customClass","tooltipOptions","placement","container","trigger","delay","fallbackPlacements","trimmedClass","tooltip","_tooltipInstance","addEventListener","addTooltipHideListeners","removeTooltipHideListeners","tooltipInstance","getInstance","dispose","removeAttribute","getSidebarState","setToggleEnabled","enabled","_tooltipScrollHandler","passive","_tooltipRouteHandler","_tooltipBlurHandler","_tooltipEscapeHandler","e","removeEventListener","onBeforeDestroy","checkMobile","isMobile","innerWidth","createDefault","createMinimal","setSidebarTheme","removeClass","collapse","expand","pulseToggle","toggleButton","removePulse","once","addSimpleMenuItem","text","icon","setSimpleMenu","PageHeader","style","showIcon","showDescription","showBreadcrumbs","currentPage","getMinimalTemplate","getBreadcrumbTemplate","getDefaultTemplate","hasPage","title","displayName","pageName","pageIcon","pageDescription","description","headerActions","pageTitle","breadcrumbs","actions","hasActions","setPage","getPage","onHeaderAction","DeniedPage","Page","template","deniedPage","deniedPageOptions","onParams","query","pageOptions","deniedPageName","setDeniedPage","pageInstance","getViewData","currentUser","getCurrentUser","deniedPageInfo","requiredPermissions","username","email","showLogin","handleActionGoBack","preventDefault","history","back","handleActionGoHome","navigateToDefault","handleActionLogin","showPage","navigate","navError","returnUrl","pathname","showInfo","onEnter","setMeta","timestamp","Date","toISOString","showForPage","NotFoundPage","path","setInfo","_element","showForPath","notFoundPage","PortalApp","WebApp","sidebarConfig","topbarConfig","topbar","topnav","showPageHeader","pageHeaderConfig","pageHeader","tokenManager","TokenManager","sidebarCollapsed","defaultCollapsed","loadSidebarState","setupPageContainer","toast","ToastService","registerPage","start","checkAuthStatus","clearTokens","rest","clearAuth","setActiveUser","checkAndRefreshTokens","onPortalAction","bind","checkActiveGroup","setupRouter","isStarted","showPasskeySetup","tokenStatus","checkTokenStatus","token","getTokenInstance","startAutoRefresh","setAuthToken","User","getUserId","resp","success","urlGroupId","groupId","loadActiveGroupId","status","clearActiveGroup","statusText","saveActiveGroupId","member","Member","fetchForGroup","clearActiveGroupId","storedGroupId","fallbackGroup","fallbackError","previousGroup","getCurrentPage","onGroupChange","updateUrl","getActiveGroup","getActiveGroupStorageKey","toString","setPortalProfile","profile","needsGroupSelection","Error","showSidebar","showTopbar","contentMarkup","innerHTML","pageContainer","setupPortalComponents","applySidebarState","setupSidebar","setupTopbar","setupPageHeader","setupPortalEvents","TopNav","brandText","brand","brandRoute","brandIcon","navItems","leftItems","rightItems","displayMode","showSidebarToggle","headerContainer","getElementById","target","closest","ResizeObserver","resizeObserver","handleResponsive","observe","_resizeObserver","_resizeHandler","saveSidebarState","collapsed","mobile","getPortalContainer","hasMobileLayout","updateNavigation","setActivePage","setUser","getActiveUser","getSidebarStorageKey","JSON","stringify","saved","parse","clearSidebarState","changePassword","showForm","fields","type","label","required","strengthMeter","capsLockWarning","passwordUsage","attributes","autocomplete","submitLabel","new_password","confirm_password","save","showProfile","UserProfileView","Promise","resolve","then","index$1","profileView","showDialog","centered","showError","PasskeySetupView","setupView","dialog","bsModal","Modal","disconnect","create","FormPage","recreateFormView","formView","getModel","FormView","autosaveModelField","setModel","mustacheFormatter","formatter","dataFormatter","compiledTemplates","partials","mustache","compile","compiled","renderCompiled","clearCache","cache","getCached","registerFormatter","register","hasPipes","test","processData","pipes","processed","pipeString","value","getValueFromPath","pipe","obj","split","current","isNaN","parseInt","processTemplate","ProfileOverviewSection","hasPhone","roleLabel","permissionPeek","perms","permMap","PERMISSIONS","p","c","slice","remaining","onActionEditUsername","submitText","placeholder","cols","POST","new_username","current_password","confirm_identity","graph","message","onActionDeactivateAccount","onActionVerifyEmail","sendResp","method","code","prompt","confirmResp","onActionVerifyPhone","phone","onActionAddPhone","saveResp","phone_number","onActionRemovePhone","onActionNavigate","ProfilePersonalSection","hasDob","dobFormatted","dob","year","month","day","timezoneDisplay","timezone","hasAddress","meta","street","city","zip","country","addressSummary","Boolean","join","onActionEditDisplayName","defaultValue","display_name","onActionEditFirstName","first_name","onActionEditLastName","last_name","onActionEditDob","onActionEditTimezone","updatedMeta","metadata","onActionEditAddress","Passkey","Model","endpoint","registerBegin","err","registerComplete","challenge_id","credential","PasskeyList","ModelClass","PasskeyForms","edit","columns","help","ProfileSecuritySection","onActionChangePassword","onActionManagePasskeys","models","passkeys","onActionEditPasskey","async","dataset","passkey","String","showModelForm","onActionDeletePasskey","class","dismiss","_registerPasskey","_base64urlToBytes","base64url","base64","padded","repeat","Uint8Array","atob","charCodeAt","beginResp","publicKey","rp","hostname","challenge","excludeCredentials","cred","navigator","credentials","friendlyName","credentialData","rawId","btoa","fromCharCode","response","clientDataJSON","attestationObject","getTransports","transports","completeResp","friendly_name","onActionManageTotp","DELETE","setupResp","dataOnly","secret","qr_code","qrCode","onActionManageRecoveryCodes","GET","codes","regenResp","recovery_codes","newCodes","codesText","newView","clipboard","writeText","onActionRevokeAllSessions","password","access_token","auth","setTokens","PROVIDER_ICONS","google","github","microsoft","apple","facebook","twitter","linkedin","ProfileConnectedSection","connections","results","provider","onActionUnlink","connection","SessionItem","ListViewItem","browserName","ud","device_info","user_agent","family","deviceName","dev","device","geo","parts","region","country_name","os","some","m","deviceIcon","ProfileSessionsSection","listView","ListView","UserDeviceLocationList","defaultQuery","itemClass","emptyMessage","DeviceItem","browserInfo","ua","major","osName","ProfileDevicesSection","UserDeviceList","SecurityEvent","SecurityEventList","DANGER_KINDS","SUCCESS_KINDS","SecurityEventRow","TableRow","kindBadgeClass","kindLabel","kindIcon","ProfileSecurityEventsSection","tableView","TableView","hideActivePillNames","visibility","sortable","searchable","filterable","paginated","showAdd","showExport","tableOptions","striped","hover","CHANNEL_LABELS","in_app","CHANNELS","ProfileNotificationsSection","preferences","channels","ch","hasPreferences","preferenceRows","toggles","channel","checked","onActionTogglePref","ProfileApiKeysSection","generatedToken","onActionGenerateKey","ipsInput","allowed_ips","ip","expireDays","expire_days","resultEl","tokenEl","display","onActionCopyToken","AVATAR_COLORS","GroupMemberItem","groupName","initials","w","avatarColor","hash","abs","roleName","role","manage_group","hasRole","roleBadgeClass","ProfileGroupsSection","MemberList","ProfilePermissionsSection","permissionTags","SECTIONS","personal","security","connected","sessions","devices","security_events","notifications","api_keys","groups","activeSection","sectionView","hasAvatar","section","SectionClass","onActionChangeAvatar","updateModelImage","field","upload","imageSize","width","height","onActionCloseDialog","modal","onActionUpdateEmail","inputmode","new_email_address","onActionUpdatePhone","new_phone","sessionToken","session_token","onActionCreatePasskey","onActionSkip","onActionDontAsk","checkbox","FRAMEWORK_NAME","PACKAGE_NAME"],"mappings":"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"}
|