import { createElement } from "../utils/dom";
import { AgentWidgetConfig } from "../types";
import { positionMap } from "../utils/positioning";
import { isDockedMountMode } from "../utils/dock";
import { renderLucideIcon } from "../utils/icons";
import { DEFAULT_OVERLAY_Z_INDEX } from "../utils/constants";
export interface LauncherButton {
element: HTMLButtonElement;
update: (config: AgentWidgetConfig) => void;
destroy: () => void;
}
export const createLauncherButton = (
config: AgentWidgetConfig | undefined,
onToggle: () => void
): LauncherButton => {
const button = createElement("button") as HTMLButtonElement;
button.type = "button";
button.innerHTML = `
💬
↗
`;
button.addEventListener("click", onToggle);
const update = (newConfig: AgentWidgetConfig) => {
const launcher = newConfig.launcher ?? {};
const dockedMode = isDockedMountMode(newConfig);
const titleEl = button.querySelector("[data-role='launcher-title']");
if (titleEl) {
const t = launcher.title ?? "Chat Assistant";
titleEl.textContent = t;
titleEl.setAttribute("title", t);
}
const subtitleEl = button.querySelector("[data-role='launcher-subtitle']");
if (subtitleEl) {
const s = launcher.subtitle ?? "Here to help you get answers fast";
subtitleEl.textContent = s;
subtitleEl.setAttribute("title", s);
}
// Hide/show text container
const textContainer = button.querySelector(".persona-flex-col");
if (textContainer) {
if (launcher.textHidden || dockedMode) {
(textContainer as HTMLElement).style.display = "none";
} else {
(textContainer as HTMLElement).style.display = "";
}
}
const icon = button.querySelector("[data-role='launcher-icon']");
if (icon) {
if (launcher.agentIconHidden) {
icon.style.display = "none";
} else {
const iconSize = launcher.agentIconSize ?? "40px";
icon.style.height = iconSize;
icon.style.width = iconSize;
// Optional custom background color for the agent icon circle. When set,
// override the default primary-color background; otherwise restore it.
if (launcher.agentIconBackgroundColor) {
icon.style.backgroundColor = launcher.agentIconBackgroundColor;
icon.classList.remove("persona-bg-persona-primary");
} else {
icon.style.backgroundColor = "";
icon.classList.add("persona-bg-persona-primary");
}
// Clear existing content
icon.innerHTML = "";
// Render icon based on priority: Lucide icon > iconUrl > agentIconText
if (launcher.agentIconName) {
// Use Lucide icon
const iconSizeNum = parseFloat(iconSize) || 24;
const iconSvg = renderLucideIcon(launcher.agentIconName, iconSizeNum * 0.6, "var(--persona-text-inverse, #ffffff)", 2);
if (iconSvg) {
icon.appendChild(iconSvg);
icon.style.display = "";
} else {
// Fallback to agentIconText if Lucide icon fails
icon.textContent = launcher.agentIconText ?? "💬";
icon.style.display = "";
}
} else if (launcher.iconUrl) {
// Use image URL - hide icon span and show img
icon.style.display = "none";
} else {
// Use text/emoji
icon.textContent = launcher.agentIconText ?? "💬";
icon.style.display = "";
}
}
}
const img = button.querySelector("[data-role='launcher-image']");
if (img) {
const iconSize = launcher.agentIconSize ?? "40px";
img.style.height = iconSize;
img.style.width = iconSize;
if (launcher.iconUrl && !launcher.agentIconName && !launcher.agentIconHidden) {
// Only show image if not using Lucide icon and not hidden
img.src = launcher.iconUrl;
img.style.display = "block";
} else {
img.style.display = "none";
}
}
const callToActionIconEl = button.querySelector("[data-role='launcher-call-to-action-icon']");
if (callToActionIconEl) {
const callToActionIconSize = launcher.callToActionIconSize ?? "32px";
callToActionIconEl.style.height = callToActionIconSize;
callToActionIconEl.style.width = callToActionIconSize;
// Apply background color if configured
if (launcher.callToActionIconBackgroundColor) {
callToActionIconEl.style.backgroundColor = launcher.callToActionIconBackgroundColor;
callToActionIconEl.classList.remove("persona-bg-persona-primary");
} else {
callToActionIconEl.style.backgroundColor = "";
callToActionIconEl.classList.add("persona-bg-persona-primary");
}
// Apply foreground/icon color if configured
if (launcher.callToActionIconColor) {
callToActionIconEl.style.color = launcher.callToActionIconColor;
callToActionIconEl.classList.remove("persona-text-persona-call-to-action");
} else {
callToActionIconEl.style.color = "";
callToActionIconEl.classList.add("persona-text-persona-call-to-action");
}
// Calculate padding to adjust icon size
let paddingTotal = 0;
if (launcher.callToActionIconPadding) {
callToActionIconEl.style.boxSizing = "border-box";
callToActionIconEl.style.padding = launcher.callToActionIconPadding;
// Parse padding value to calculate total padding (padding applies to both sides)
const paddingValue = parseFloat(launcher.callToActionIconPadding) || 0;
paddingTotal = paddingValue * 2; // padding on both sides
} else {
callToActionIconEl.style.boxSizing = "";
callToActionIconEl.style.padding = "";
}
if (launcher.callToActionIconHidden) {
callToActionIconEl.style.display = "none";
} else {
callToActionIconEl.style.display = dockedMode ? "none" : "";
// Clear existing content
callToActionIconEl.innerHTML = "";
// Use Lucide icon if provided, otherwise fall back to text
if (launcher.callToActionIconName) {
// Calculate actual icon size by subtracting padding
const containerSize = parseFloat(callToActionIconSize) || 24;
const iconSize = Math.max(containerSize - paddingTotal, 8); // Ensure minimum size of 8px
const iconSvg = renderLucideIcon(launcher.callToActionIconName, iconSize, "currentColor", 2);
if (iconSvg) {
callToActionIconEl.appendChild(iconSvg);
} else {
// Fallback to text if icon fails to render
callToActionIconEl.textContent = launcher.callToActionIconText ?? "↗";
}
} else {
callToActionIconEl.textContent = launcher.callToActionIconText ?? "↗";
}
}
}
const positionClass =
launcher.position && positionMap[launcher.position]
? positionMap[launcher.position]
: positionMap["bottom-right"];
const floatingBase =
"persona-fixed persona-flex persona-items-center persona-gap-3 persona-rounded-launcher persona-bg-persona-surface persona-py-2.5 persona-pl-3 persona-pr-3 persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
const dockedBase =
"persona-relative persona-mt-4 persona-mb-4 persona-mx-auto persona-flex persona-items-center persona-justify-center persona-rounded-launcher persona-bg-persona-surface persona-transition hover:persona-translate-y-[-2px] persona-cursor-pointer";
button.className = dockedMode ? dockedBase : `${floatingBase} ${positionClass}`;
if (!dockedMode) {
button.style.zIndex = String(launcher.zIndex ?? DEFAULT_OVERLAY_Z_INDEX);
}
// Apply launcher border and shadow from config (with defaults matching previous Tailwind classes)
const defaultBorder = "1px solid var(--persona-border, #e5e7eb)";
const defaultShadow = "var(--persona-launcher-shadow, 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1))";
button.style.border = launcher.border ?? defaultBorder;
button.style.boxShadow =
launcher.shadow !== undefined
? (launcher.shadow.trim() === "" ? "none" : launcher.shadow)
: defaultShadow;
if (dockedMode) {
// Docked mode uses a 0px column when closed and hides this button; keep no hit target.
button.style.width = "0";
button.style.minWidth = "0";
button.style.maxWidth = "0";
button.style.padding = "0";
button.style.overflow = "hidden";
button.style.border = "none";
button.style.boxShadow = "none";
} else {
button.style.width = "";
button.style.minWidth = "";
button.style.maxWidth = launcher.collapsedMaxWidth ?? "";
button.style.justifyContent = "";
button.style.padding = "";
button.style.overflow = "";
}
};
const destroy = () => {
button.removeEventListener("click", onToggle);
button.remove();
};
// Initial update
if (config) {
update(config);
}
return {
element: button,
update,
destroy
};
};