studiocms
Version:
Astro Native CMS for AstroDB. Built from the ground up by the Astro community.
665 lines (646 loc) • 23.3 kB
JavaScript
const PERMISSION_HIERARCHY = {
owner: /* @__PURE__ */ new Set(["owner"]),
admin: /* @__PURE__ */ new Set(["owner", "admin"]),
editor: /* @__PURE__ */ new Set(["owner", "admin", "editor"]),
visitor: /* @__PURE__ */ new Set(["owner", "admin", "editor", "visitor"]),
unknown: /* @__PURE__ */ new Set(["owner", "admin", "editor", "visitor", "unknown"])
};
const KNOWN_API_ROUTES = ["/studiocms_api/", "/_studiocms-devapps/", "/_web-vitals"];
const DEFAULT_AVATAR = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADSklEQVR4AdSWS2xMYRTHjQXRRupRKVWrJhrxSCRigTQpYUHq0VYsarwi8RgJ0rCwFJEQEQ1psZFQXWgGRZB6NdFalkSFFRutV1UJNhLj9+dcOubeud8dC2lzfvd/7vedc77TO999DB/2n/+GZgOpVCoOSegx5K/N5WJGugIsNhHusNBZqIJiQ/455tqgiDFni9QAVZthIVyBBTAWJsBKeAOL4Tw4m3MD/GebqVoBV2Ox2ArohAHog1bGZ0MfLCJ2A+pkzg1QrRpkB3X4G5p4xdhOkOknkYYSpYE5Vu2pqZ/ctUFdDXOzS5QGvlkplxyXmJ/lnAOJfgSyMh0C0AbVVJcOLkRp4JQV3GeaJmy8SQzUg6xRBxecG2CTaaffpGgli12DeVAAhaDb8CFzhXCZ2Ouokzk3YNU2ordhGXTCALyDS6AH0C10CzhbpAb4z16DHjZxVtCivWgPJCHO3BJ4i+9skRrwqrJIE1TBZCiBGmjy5qNoTg1EWSAsNlIDbLZi2AEt0A2fDPkX8BOguyFs3d/zTg1QNB+OkPUcjkMNTIfRhvzV+CfgBbGHYBR+qIU2QCG97dqpVAcjoRuOgpqYggr5GnvCuWL2ou3kjkOzWtYGKFBAdgfoPfAR1S02iw1XB0l4achXgzOJ2QaKnYt2WA1cfwtsgETNtZA2FXR/l7LYaUhx7muag5NMloI+XKahzdSKob6mRXwnGEyA7nm949dQ+D3nTmax2hN6SC0lSVcOyTTfBuhYl36/hddS8IP5zmI56y3hADW1Ye30j/g2wLQ21Ri0i0JtaE5G7g0SH8N40PsCSbegBiot7J7pv4hXw6uZViuogRkWpdvP3JzFq6E7JKNIUAP63FbwJn67KsjTSRSUA/o2XGd5Xk07/SVBDei7/zshq0Bvun6KtcIeqIUKKAM9IfNQ+RrTnGL07dBvufrtVesM5xnm2wCbZyuRer9vR+/DCFgOh0FvPX18PsP/DF9AvsY0pxjFKke5up2LqLmLuAzzbUBRJOh7vxEt57wEVKABvQgPQO+Fr6iQrzHNKWY343pNl5PfAHqWMJRpgQ0MDqVAL9RDAqphPujJmI8K+RrTnGKOMa6PlcFlfH2nBnwzHQfDwn4AAAD//6qWhy8AAAAGSURBVAMAJXQ0UKI3Vu0AAAAASUVORK5CYII=";
const COMPONENT_STYLES = `
:host {
--border: hsl(240 5% 17%);
--background-base: hsl(0 0% 6%);
--background-step-1: hsl(0 0% 8%);
--background-step-2: hsl(0 0% 10%);
--background-step-3: hsl(0 0% 14%);
--primary-base: hsl(259 83% 73%);
--success-base: hsl(142 71% 46%);
--warning-base: hsl(48 96% 53%);
--danger-base: hsl(339 97% 31%);
--info-base: hsl(217 92% 52%);
--light: 70;
--threshold: 50;
}
[data-theme="light"] {
--border: hsl(263 5% 68%);
--background-base: hsl(0 0% 97%);
--background-step-1: hsl(0 0% 90%);
--background-step-2: hsl(0 0% 85%);
--background-step-3: hsl(0 0% 80%);
--primary-base: hsl(259 85% 61%);
--success-base: hsl(142 59% 47%);
--warning-base: hsl(48 92% 46%);
--danger-base: hsl(339 97% 31%);
--info-base: hsl(217 92% 52%);
}
.menu_overlay {
position: fixed;
background: rgba(0,0,0,0.4);
inset: 0;
z-index: 500;
display: none;
}
.menu_overlay.menuOpened {
display: block;
}
.cornerMenu {
position: fixed;
right: 25px;
bottom: 25px;
width: 50px;
height: 50px;
background: var(--background-step-1);
box-shadow: 0 3px 7px rgba(0,0,0,0.3);
border-radius: 50%;
z-index: 600;
cursor: pointer;
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-container {
width: 100%;
height: 100%;
border-radius: 50%;
background: var(--background-step-1);
border: 1px solid var(--border);
object-fit: cover;
z-index: 700;
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.avatar {
width: 100%;
height: 100%;
background: var(--background-step-1);
border: 1px solid var(--border);
border-radius: 50%;
object-fit: cover;
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-error {
width: 100%;
height: auto;
margin: 2.5rem;
border: none;
}
.cornerMenu.menuOpened .avatar-container {
transform: scale(1.5);
border: 1px solid var(--border);
}
.menu {
--switch: calc((var(--light) - var(--threshold)) * -100%);
position: absolute;
width: 32px;
height: 32px;
background: var(--background-step-2);
box-shadow: 0 3px 7px rgba(0,0,0,0.1);
border-radius: 50%;
border: 1px solid var(--border);
color: hsl(0, 0%, var(--switch));
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
top: -5px;
left: -5px;
opacity: 0;
z-index: 550;
pointer-events: none;
user-select: none;
transition: all 0.4s ease-in-out;
text-decoration: none;
}
.menu svg {
width: 24px;
height: 24px;
}
.cornerMenu.menuOpened .menu {
opacity: 1;
cursor: pointer;
transition: transform 0.3s ease, opacity 0.3s ease, background-color 0.15s ease;
}
.cornerMenu.menuOpened .menu:hover {
background: var(--background-step-3);
}
/* Click protection: Only enable pointer events when menu is ready */
.cornerMenu.menu-ready .menu {
pointer-events: all;
box-shadow: 0 3px 7px rgba(0,0,0,0.1),
0 0 0 1px color-mix(in hsl, var(--primary-base) 20%, transparent);
}
.cornerMenu.menu-ready .menu:hover {
box-shadow: 0 3px 7px rgba(0,0,0,0.2),
0 0 0 2px color-mix(in hsl, var(--primary-base) 40%, transparent);
}
/* Visual feedback for ignored clicks */
.menu.click-ignored {
animation: shake 0.3s ease-in-out;
}
shake {
0%, 100% { transform: translateX(0) translateY(0); }
25% { transform: translateX(-2px) translateY(-1px); }
50% { transform: translateX(2px) translateY(1px); }
75% { transform: translateX(-1px) translateY(-2px); }
}
.cornerMenu.menuOpened .menu:nth-child(1) { transform: translate(-105px, 20px); transition-delay: 0s; }
.cornerMenu.menuOpened .menu:nth-child(2) { transform: translate(-78px, -33px); transition-delay: 0.05s; }
.cornerMenu.menuOpened .menu:nth-child(3) { transform: translate(-38px, -76px); transition-delay: 0.1s; }
.cornerMenu.menuOpened .menu:nth-child(4) { transform: translate(18px, -99px); transition-delay: 0.15s; }
.menu.logout { color: var(--danger-base); }
.menu.profile { color: var(--primary-base); }
.menu.dashboard { color: var(--success-base); }
.menu.edit { color: var(--warning-base); }
`;
function verifyUserPermissionLevel(userLevel, requiredLevel) {
return PERMISSION_HIERARCHY[requiredLevel]?.has(userLevel) ?? false;
}
function shouldSkipRendering(pathname) {
return KNOWN_API_ROUTES.some((route) => pathname.includes(route));
}
function isDashboardRoute(pathname, dashboardRoute) {
return pathname.includes(dashboardRoute);
}
class UserQuickTools extends HTMLElement {
sessionData = null;
isMenuOpen = false;
menuItemsReady = false;
lastMenuToggleTime = 0;
readyTimeout = null;
themeObserver = null;
cornerMenu = null;
menuOverlay = null;
isInitialized = false;
userInteractionListeners = [];
// Click protection settings
CLICK_PROTECTION_DURATION = 400;
// milliseconds
MENU_READY_DELAY = 350;
// milliseconds (after animation completes)
// Static menu items configuration
static MENU_ITEMS = [
{
name: "Logout",
svg: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15" /></svg>',
permission: "visitor",
cssClass: "logout"
},
{
name: "Profile",
svg: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>',
permission: "visitor",
cssClass: "profile"
},
{
name: "Dashboard",
svg: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-2.25Z" /></svg>',
permission: "editor",
cssClass: "dashboard"
},
{
name: "Edit",
svg: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /></svg>',
permission: "editor",
cssClass: "edit"
}
];
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
const pathname = window.location.pathname;
if (shouldSkipRendering(pathname)) {
return;
}
this.initOnUserInteraction();
}
/* v8 ignore stop */
initOnUserInteraction() {
const interactionEvents = [
"mouseenter",
"mousemove",
"touchstart",
"scroll",
"keydown",
"click"
];
const handleUserInteraction = () => {
if (this.isInitialized) return;
this.removeInteractionListeners();
this.scheduleInitialization();
};
interactionEvents.forEach((eventType) => {
const listener = handleUserInteraction;
document.addEventListener(eventType, listener, {
passive: true,
once: true
// Automatically removes after first trigger
});
this.userInteractionListeners.push({ event: eventType, handler: listener });
});
}
removeInteractionListeners() {
this.userInteractionListeners.forEach(({ event, handler }) => {
document.removeEventListener(event, handler);
});
this.userInteractionListeners = [];
}
scheduleInitialization() {
if (this.isInitialized) return;
this.isInitialized = true;
const initializeComponent = () => {
const pathname = window.location.pathname;
this.initializeAsync(pathname).catch((error) => {
console.error("UserQuickTools initialization failed:", error);
});
};
if ("requestIdleCallback" in window) {
requestIdleCallback(initializeComponent, { timeout: 1e3 });
} else {
setTimeout(initializeComponent, 0);
}
}
async initializeAsync(pathname) {
try {
const sessionData = await this.getSession();
if (!sessionData?.isLoggedIn) {
return;
}
if (isDashboardRoute(pathname, sessionData.routes.dashboardIndex)) {
return;
}
this.sessionData = sessionData;
this.scheduleRender();
} catch (error) {
console.warn("UserQuickTools failed to initialize:", error);
}
}
/* v8 ignore start */
scheduleRender() {
const renderComponent = () => {
this.render();
this.setupEventListeners();
this.setupThemeObserver();
};
requestAnimationFrame(() => {
if ("requestIdleCallback" in window) {
requestIdleCallback(renderComponent, { timeout: 500 });
} else {
setTimeout(renderComponent, 0);
}
});
}
disconnectedCallback() {
this.cleanup();
this.removeInteractionListeners();
}
async render() {
if (!this.shadowRoot || !this.sessionData) return;
this.menuOverlay = document.createElement("div");
this.menuOverlay.className = "menu_overlay";
this.cornerMenu = document.createElement("div");
this.cornerMenu.className = "cornerMenu";
this.cornerMenu.dataset.theme = document.documentElement.dataset.theme ?? "dark";
const styleElm = document.createElement("style");
styleElm.textContent = COMPONENT_STYLES;
this.addMenuItems();
this.shadowRoot.append(styleElm, this.menuOverlay, this.cornerMenu);
void this.addUserAvatar().catch((e) => console.warn("Avatar load failed:", e));
}
addMenuItems() {
if (!this.cornerMenu || !this.sessionData) return;
const { routes, permissionLevel } = this.sessionData;
const routeMap = {
Logout: routes.logout,
Profile: routes.userProfile,
Dashboard: routes.dashboardIndex,
Edit: routes.contentManagement
};
UserQuickTools.MENU_ITEMS.forEach((item) => {
if (verifyUserPermissionLevel(permissionLevel, item.permission)) {
const menuElement = this.createMenuElement({
...item,
href: routeMap[item.name]
});
this.cornerMenu.appendChild(menuElement);
}
});
}
createMenuElement(item) {
if (item.name === "Logout") {
return this.createLogoutElement(item);
}
const element = document.createElement("a");
element.className = `menu ${item.cssClass}`;
element.title = item.name;
const parser = new DOMParser();
const svgDoc = parser.parseFromString(item.svg, "image/svg+xml");
const svgElement = svgDoc.documentElement;
if (svgElement && svgElement.nodeName === "svg") {
element.appendChild(svgElement.cloneNode(true));
} else {
console.warn("Invalid SVG content for menu item:", item.name);
}
element.href = item.href;
element.addEventListener("click", (e) => {
const timeSinceToggle = Date.now() - this.lastMenuToggleTime;
if (!this.menuItemsReady || timeSinceToggle < this.CLICK_PROTECTION_DURATION) {
e.preventDefault();
e.stopPropagation();
element.classList.add("click-ignored");
setTimeout(() => element.classList.remove("click-ignored"), 300);
return false;
}
});
return element;
}
createLogoutElement(item) {
const element = document.createElement("a");
element.className = `menu ${item.cssClass}`;
element.title = item.name;
element.href = "#";
const parser = new DOMParser();
const svgDoc = parser.parseFromString(item.svg, "image/svg+xml");
const svgElement = svgDoc.documentElement;
if (svgElement && svgElement.nodeName === "svg") {
element.appendChild(svgElement.cloneNode(true));
}
element.addEventListener("click", (e) => {
e.preventDefault();
const timeSinceToggle = Date.now() - this.lastMenuToggleTime;
if (!this.menuItemsReady || timeSinceToggle < this.CLICK_PROTECTION_DURATION) {
element.classList.add("click-ignored");
setTimeout(() => element.classList.remove("click-ignored"), 300);
return;
}
this.submitLogoutForm(item.href);
});
return element;
}
submitLogoutForm(logoutUrl) {
const form = document.createElement("form");
form.method = "POST";
form.action = logoutUrl;
form.style.display = "none";
document.body.appendChild(form);
form.submit();
}
async testAvatarURL(url) {
let urlObj;
try {
urlObj = new URL(url);
} catch {
console.warn("Invalid avatar URL:", url);
return void 0;
}
if (urlObj.protocol !== "https:") {
console.error(`Insecure avatar URL protocol: ${urlObj.protocol}`);
return void 0;
}
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 4e3);
try {
const response = await fetch(url, {
method: "HEAD",
signal: controller.signal,
cache: "no-cache"
});
if (!response.ok) return void 0;
const contentType = (response.headers.get("content-type") || "").split(";")[0].trim();
if (!contentType.startsWith("image/")) return void 0;
if (contentType === "image/svg+xml") {
console.error("Remote SVG avatars are disallowed for security.");
return void 0;
}
return { type: contentType };
} catch (err) {
console.warn("Avatar HEAD check failed:", err);
return void 0;
} finally {
clearTimeout(timeoutId);
}
}
async addUserAvatar() {
if (!this.cornerMenu || !this.sessionData) return;
const { user, permissionLevel } = this.sessionData;
const avatarContainer = document.createElement("div");
avatarContainer.className = "avatar-container";
const newAvatar = document.createElement("img");
const avatar = await (async () => {
let avatar2 = DEFAULT_AVATAR;
if (user.avatar) {
const result = await this.testAvatarURL(user.avatar);
if (result) {
avatar2 = user.avatar;
}
}
return avatar2;
})();
newAvatar.src = avatar;
newAvatar.width = 64;
newAvatar.height = 64;
newAvatar.className = "avatar";
newAvatar.alt = `${user.name} - ${this.capitalizeFirst(permissionLevel)}`;
newAvatar.loading = "lazy";
newAvatar.decoding = "async";
newAvatar.referrerPolicy = "no-referrer";
if (avatar === DEFAULT_AVATAR) {
newAvatar.classList.add("avatar-error");
}
newAvatar.onerror = function() {
this.src = DEFAULT_AVATAR;
};
newAvatar.setAttribute("aria-hidden", "true");
avatarContainer.appendChild(newAvatar);
this.cornerMenu.appendChild(avatarContainer);
}
setupEventListeners() {
if (!this.cornerMenu || !this.menuOverlay) return;
this.cornerMenu.addEventListener("click", this.handleMenuToggle.bind(this));
this.menuOverlay.addEventListener("click", this.handleOverlayClick.bind(this));
}
handleMenuToggle() {
this.lastMenuToggleTime = Date.now();
this.isMenuOpen = !this.isMenuOpen;
if (this.isMenuOpen) {
this.menuItemsReady = false;
this.cornerMenu?.classList.remove("menu-ready");
this.readyTimeout = window.setTimeout(() => {
this.menuItemsReady = true;
this.cornerMenu?.classList.add("menu-ready");
}, this.MENU_READY_DELAY);
} else {
this.menuItemsReady = false;
this.cornerMenu?.classList.remove("menu-ready");
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
}
this.updateMenuState();
}
handleOverlayClick() {
if (this.isMenuOpen) {
this.isMenuOpen = false;
this.menuItemsReady = false;
this.cornerMenu?.classList.remove("menu-ready");
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
this.updateMenuState();
}
}
updateMenuState() {
if (!this.cornerMenu || !this.menuOverlay) return;
const method = this.isMenuOpen ? "add" : "remove";
this.cornerMenu.classList[method]("menuOpened");
this.menuOverlay.classList[method]("menuOpened");
}
setupThemeObserver() {
if (!this.cornerMenu) return;
const updateTheme = () => {
const theme = document.documentElement.getAttribute("data-theme");
this.cornerMenu.dataset.theme = theme === "light" ? "light" : "dark";
};
this.themeObserver = new MutationObserver(updateTheme);
this.themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"]
});
}
/* v8 ignore stop */
async getSession() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5e3);
const response = await fetch("/studiocms_api/dashboard/verify-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ originPathname: window.location.toString() }),
signal: controller.signal
});
clearTimeout(timeoutId);
return response.ok ? await response.json() : null;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.warn("Session verification timed out");
} else {
console.warn("Session verification failed:", error);
}
return null;
}
}
/* v8 ignore start */
capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
cleanup() {
this.themeObserver?.disconnect();
this.themeObserver = null;
if (this.readyTimeout) {
clearTimeout(this.readyTimeout);
this.readyTimeout = null;
}
this.cornerMenu = null;
this.menuOverlay = null;
this.sessionData = null;
this.isInitialized = false;
this.menuItemsReady = false;
this.lastMenuToggleTime = 0;
}
/* v8 ignore stop */
}
class ConfigurableUserQuickTools extends UserQuickTools {
config;
constructor() {
super();
this.config = {
strategy: this.getAttribute("data-init-strategy") || "interaction",
timeout: Number.parseInt(this.getAttribute("data-timeout") || "1000", 10),
clickProtectionDuration: Number.parseInt(
this.getAttribute("data-click-protection") || "400",
10
),
menuReadyDelay: Number.parseInt(this.getAttribute("data-menu-delay") || "350", 10)
};
if (this.config.clickProtectionDuration) {
this.CLICK_PROTECTION_DURATION = this.config.clickProtectionDuration;
}
if (this.config.menuReadyDelay) {
this.MENU_READY_DELAY = this.config.menuReadyDelay;
}
}
connectedCallback() {
const pathname = window.location.pathname;
if (shouldSkipRendering(pathname)) {
return;
}
switch (this.config.strategy) {
case "immediate":
this.scheduleInitialization();
break;
/* v8 ignore stop */
case "idle":
if ("requestIdleCallback" in window) {
requestIdleCallback(() => this.scheduleInitialization(), {
timeout: this.config.timeout
});
} else {
setTimeout(() => this.scheduleInitialization(), 0);
}
break;
/* v8 ignore stop */
case "interaction":
this.initOnUserInteraction();
break;
default:
console.warn(`Unknown initialization strategy: ${this.config.strategy}`);
this.initOnUserInteraction();
break;
}
}
}
if ("customElements" in window && !customElements.get("user-quick-tools")) {
customElements.define("user-quick-tools", ConfigurableUserQuickTools);
}
function initializeWhenReady() {
const createElement = () => {
if (!document.querySelector("user-quick-tools")) {
const element = document.createElement("user-quick-tools");
element.setAttribute("data-init-strategy", "idle");
element.setAttribute("data-timeout", "1000");
element.setAttribute("data-click-protection", "400");
element.setAttribute("data-menu-delay", "350");
document.body.appendChild(element);
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
setTimeout(createElement, 0);
});
} else {
setTimeout(createElement, 0);
}
}
initializeWhenReady();
export {
COMPONENT_STYLES,
ConfigurableUserQuickTools,
DEFAULT_AVATAR,
KNOWN_API_ROUTES,
PERMISSION_HIERARCHY,
UserQuickTools,
initializeWhenReady,
isDashboardRoute,
shouldSkipRendering,
verifyUserPermissionLevel
};