@web-widget/inspector
Version:
Web Widget Inspector - Development toolbar for web-widget elements
1,427 lines (1,374 loc) • 40.7 kB
JavaScript
import { css, LitElement, html } from 'lit';
import { property, state } from 'lit/decorators.js';
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? undefined : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
// src/utils/widget-finder.ts
function findWebWidgetContainer(element) {
if (!element) {
return null;
}
if (element.tagName === "WEB-WIDGET" && element.hasAttribute("import")) {
return element;
}
const container = element.closest("web-widget[import]");
return container;
}
// src/utils/storage.ts
function getStoredValue(key, defaultValue) {
const stored = localStorage.getItem(key);
if (stored === null) {
return defaultValue;
}
try {
return JSON.parse(stored);
} catch {
return defaultValue;
}
}
function setStoredValue(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
// src/utils/design-system.ts
var spacing = {
xs: "0.25rem",
sm: "0.5rem",
md: "0.75rem",
lg: "1rem",
xl: "1.25rem"
};
var typography = {
fontFamily: {
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
},
fontSize: {
xs: "0.75rem",
sm: "0.875rem",
base: "1rem"
},
fontWeight: {
normal: "400",
medium: "500",
semibold: "600"
},
lineHeight: {
normal: "1.5"
},
letterSpacing: {
normal: "0"
}
};
var boxShadow = {
base: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"
};
var zIndex = {
TOOLTIP: 2147483647,
TOOLBAR: 2147483646,
OVERLAY: 2147483645
};
var colors = {
green: "#22c55e",
// Primary green
overlayBorder: "#22c55e"
};
var themes = {
light: {
bg: "rgba(255, 255, 255, 0.92)",
fg: "#222",
fgHover: "#111",
fgSelected: "#000",
border: "#e5e7eb",
accent: "#888",
accentHover: "#666",
accentSelected: "#444",
primary: colors.green,
primaryHover: "#16a34a",
primarySelected: "#15803d",
overlayBorder: colors.overlayBorder
},
dark: {
bg: "rgba(17, 17, 17, 0.92)",
fg: "#f3f4f6",
fgHover: "#ffffff",
fgSelected: "#ffffff",
border: "#374151",
accent: "#9ca3af",
accentHover: "#d1d5db",
accentSelected: "#f3f4f6",
primary: colors.green,
primaryHover: "#4ade80",
primarySelected: "#16a34a",
overlayBorder: colors.overlayBorder
}
};
function generateCSSVariables() {
return {
// Typography - only used values
"--wwi-font-sans": typography.fontFamily.sans,
"--wwi-font-size-xs": typography.fontSize.xs,
"--wwi-font-size-sm": typography.fontSize.sm,
"--wwi-font-size-base": typography.fontSize.base,
"--wwi-font-weight-normal": typography.fontWeight.normal,
"--wwi-font-weight-medium": typography.fontWeight.medium,
"--wwi-font-weight-semibold": typography.fontWeight.semibold,
"--wwi-line-height-normal": typography.lineHeight.normal,
"--wwi-letter-spacing-normal": typography.letterSpacing.normal,
// Spacing - only used values
"--wwi-spacing-xs": spacing.xs,
"--wwi-spacing-sm": spacing.sm,
"--wwi-spacing-md": spacing.md,
"--wwi-spacing-lg": spacing.lg,
"--wwi-spacing-xl": spacing.xl,
// Box shadow - only used values
"--wwi-shadow-base": boxShadow.base,
// Z-index - only used values
"--wwi-z-tooltip": zIndex.TOOLTIP.toString(),
"--wwi-z-toolbar": zIndex.TOOLBAR.toString(),
"--wwi-z-overlay": zIndex.OVERLAY.toString(),
// Brand colors - only used values
"--wwi-green": colors.green,
"--wwi-overlay-border": colors.overlayBorder
};
}
var designSystem = {
spacing,
typography,
boxShadow,
zIndex,
colors,
themes,
cssVariables: generateCSSVariables()
};
var DESIGN_SYSTEM = {
spacing,
typography,
boxShadow,
zIndex,
colors
};
function getThemeVariables(theme) {
return themes[theme];
}
// src/utils/theme.ts
function applyTheme(vars) {
const existingStyle = document.getElementById("wwi-design-system-styles");
if (existingStyle) {
existingStyle.remove();
}
const style = document.createElement("style");
style.id = "wwi-design-system-styles";
const cssVariables = Object.entries(designSystem.cssVariables).map(([property2, value]) => ` ${property2}: ${value};`).join("\n");
const themeVariables = `
--wwi-bg: ${vars.bg};
--wwi-fg: ${vars.fg};
--wwi-border: ${vars.border};
--wwi-accent: ${vars.accent};
`;
style.textContent = `
:root {
${cssVariables}
${themeVariables}
}
`;
document.head.appendChild(style);
}
function detectAutoTheme() {
if (typeof document === "undefined" || !document.body || !document.documentElement) {
return undefined;
}
const bodyStyle = getComputedStyle(document.body);
let bg = bodyStyle?.backgroundColor;
if (!bg || bg === "rgba(0, 0, 0, 0)" || bg === "transparent") {
const docStyle = getComputedStyle(document.documentElement);
bg = docStyle?.backgroundColor;
}
let isTransparent = false;
if (!bg || bg === "rgba(0, 0, 0, 0)" || bg === "transparent") {
bg = "rgb(255,255,255)";
isTransparent = true;
}
let r = 255;
let g = 255;
let b = 255;
const match = bg.match(/rgba?\(([^)]+)\)/);
if (match) {
const parts = match[1]?.split(",").map((x) => parseFloat(x.trim())) ?? [];
r = parts[0] ?? 255;
g = parts[1] ?? 255;
b = parts[2] ?? 255;
}
const brightness = (r * 299 + g * 587 + b * 114) / 1e3;
if (isTransparent || Number.isNaN(brightness) || brightness >= 128) {
return "light";
} else {
return "dark";
}
}
function applyThemeMode(mode) {
if (mode !== "auto") {
const theme = getThemeVariables(mode);
if (theme) {
applyTheme(theme);
}
return;
}
const autoTheme = detectAutoTheme();
if (autoTheme) {
const theme = getThemeVariables(autoTheme);
if (theme) {
applyTheme(theme);
} else {
applyTheme(getThemeVariables("light"));
}
} else {
applyTheme(getThemeVariables("light"));
}
}
// src/utils/box.ts
function isValidRect(rect) {
return rect.width > 0 || rect.height > 0;
}
function mergeRects(rects) {
if (rects.length === 0) return null;
let top = Infinity, left = Infinity, right = -Infinity, bottom = -Infinity;
for (const rect of rects) {
top = Math.min(top, rect.top);
left = Math.min(left, rect.left);
right = Math.max(right, rect.right);
bottom = Math.max(bottom, rect.bottom);
}
return {
top,
left,
right,
bottom,
width: right - left,
height: bottom - top
};
}
function getNodeRectsDeep(node) {
const rects = [];
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node;
const style = getComputedStyle(el);
if (style.display === "none") return [];
const rect = el.getBoundingClientRect();
if (isValidRect(rect)) rects.push(rect);
const children = el.shadowRoot ? Array.from(el.shadowRoot.children) : Array.from(el.children);
for (const child of children) {
rects.push(...getNodeRectsDeep(child));
}
if (el.tagName === "SLOT") {
const assigned = el.assignedNodes({ flatten: true });
for (const assignedNode of assigned) {
rects.push(...getNodeRectsDeep(assignedNode));
}
}
} else if (node.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
range.selectNodeContents(node);
const rect = range.getBoundingClientRect();
if (isValidRect(rect)) rects.push(rect);
}
return rects;
}
function getElementBox(el) {
const allRects = getNodeRectsDeep(el);
return mergeRects(allRects);
}
// src/utils/debug-data.ts
function collectElementData(element) {
const bounds = getElementBox(element) ?? {
left: 0,
top: 0,
right: 0,
bottom: 0,
width: 0,
height: 0
};
const isWebWidget = element.tagName === "WEB-WIDGET";
const data = {
tag: element.tagName.toLowerCase(),
bounds,
isWebWidget
};
if (element.id) {
data.id = element.id;
}
if (isWebWidget) {
data.webWidgetData = collectWebWidgetData(element);
}
return data;
}
function collectWebWidgetData(element) {
const data = {};
const webWidgetElement = element;
const name = element.getAttribute("name");
if (name) {
data.name = name;
}
const importUrl = element.getAttribute("import");
if (importUrl) {
data.module = importUrl;
}
const loading = webWidgetElement.loading;
if (loading && loading !== "eager") {
data.loading = loading;
}
const renderTarget = webWidgetElement.renderTarget;
if (renderTarget && renderTarget !== "light") {
data.renderTarget = renderTarget;
}
const status = webWidgetElement.status;
if (status) {
data.status = status;
}
if (webWidgetElement.inactive) {
data.inactive = true;
}
const contextData = webWidgetElement.contextData;
if (contextData && typeof contextData === "object") {
data.contextData = contextData;
}
const performanceData = collectPerformanceData(element);
if (performanceData.loadTime !== undefined) {
data.loadTime = performanceData.loadTime;
}
if (performanceData.mountTime !== undefined) {
data.mountTime = performanceData.mountTime;
}
return data;
}
function collectPerformanceData(element) {
const data = {};
const webWidgetElement = element;
const performanceData = webWidgetElement.performance;
if (performanceData && typeof performanceData === "object") {
if (performanceData.loadTime) {
const loadTimeStr = performanceData.loadTime;
if (typeof loadTimeStr === "string" && loadTimeStr.endsWith("ms")) {
data.loadTime = parseInt(loadTimeStr, 10);
}
}
if (performanceData.mountTime) {
const mountTimeStr = performanceData.mountTime;
if (typeof mountTimeStr === "string" && mountTimeStr.endsWith("ms")) {
data.mountTime = parseInt(mountTimeStr, 10);
}
}
}
return data;
}
function formatDebugData(data, element) {
const items = [];
if (data.isWebWidget && data.webWidgetData) {
const widgetData = data.webWidgetData;
const componentName = element.getAttribute("name");
if (componentName) {
items.push({ key: "Name", value: componentName, priority: 1 });
}
if (widgetData.module) {
items.push({ key: "Module", value: widgetData.module, priority: 2 });
}
if (widgetData.loadTime !== undefined) {
items.push({
key: "Load Time",
value: `${widgetData.loadTime}ms`,
priority: 3
});
}
if (widgetData.mountTime !== undefined) {
items.push({
key: "Mount Time",
value: `${widgetData.mountTime}ms`,
priority: 3
});
}
if (widgetData.status) {
items.push({ key: "Status", value: widgetData.status, priority: 4 });
}
if (widgetData.loading) {
items.push({ key: "Loading", value: widgetData.loading, priority: 4 });
}
if (widgetData.renderTarget) {
items.push({
key: "Render",
value: widgetData.renderTarget,
priority: 4
});
}
if (widgetData.contextData) {
const dataKeys = Object.keys(widgetData.contextData);
if (dataKeys.length > 0) {
items.push({
key: "Parameters",
value: `${dataKeys.length} keys`,
priority: 5
});
}
}
} else {
items.push({ key: "Tag", value: data.tag, priority: 1 });
if (data.id) {
items.push({ key: "ID", value: data.id, priority: 2 });
}
}
items.push({
key: "Size",
value: `${Math.round(data.bounds.width)}\xD7${Math.round(data.bounds.height)}px`,
priority: 10
});
return items.sort((a, b) => a.priority - b.priority);
}
function getPriorityClass(priority) {
if (priority <= 3) return "priority-high";
if (priority <= 6) return "priority-medium";
return "priority-low";
}
// src/element.ts
var HTMLWebWidgetInspectorElement = class extends LitElement {
constructor() {
super(...arguments);
this.dir = "";
this.keys = [];
this.pageSource = "";
this._theme = "auto";
this.isToolbarVisible = true;
this.isInspectorMode = false;
this.isMinimized = false;
this.hoveredElement = null;
this.widgetCount = 0;
this.pressedKeys = /* @__PURE__ */ new Set();
this.tooltipElement = null;
this.currentElementBounds = {
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 0,
height: 0
};
this.overlayElement = null;
}
get theme() {
return this._theme;
}
set theme(value) {
const oldValue = this._theme;
this._theme = value;
this.setAttribute("theme", value);
if (oldValue !== value) {
this.initializeTheme();
}
}
connectedCallback() {
super.connectedCallback();
this.initializeTheme();
this.initializeEventListeners();
this.updateWidgetCount();
this.loadStoredState();
this.loadRouteModuleSourceFromHeaders();
}
disconnectedCallback() {
super.disconnectedCallback();
this.cleanupInspectorElements();
}
initializeTheme() {
applyThemeMode(this.theme);
}
initializeEventListeners() {
document.addEventListener("keydown", (e) => this.handleKeyEvent(e, true), {
capture: true
});
document.addEventListener("keyup", (e) => this.handleKeyEvent(e, false), {
capture: true
});
window.addEventListener("blur", this.clearKeys.bind(this), {
capture: true
});
document.addEventListener("mousemove", this.handleHoverEvent.bind(this), {
capture: true
});
document.addEventListener("mouseover", this.handleHoverEvent.bind(this), {
capture: true
});
document.addEventListener("click", this.handleClickEvent.bind(this), {
capture: true
});
document.addEventListener("contextmenu", this.clearKeys.bind(this), {
capture: true
});
window.addEventListener("resize", this.updateState.bind(this));
document.addEventListener("scroll", this.updateState.bind(this));
}
loadStoredState() {
this.isToolbarVisible = getStoredValue(
"web-widget-inspector-visible",
true
);
this.isMinimized = getStoredValue("web-widget-inspector-minimized", false);
}
handleKeyEvent(event, add) {
if (event.code === "Escape" && this.isInspectorMode) {
this.isInspectorMode = false;
this.updateState();
event.preventDefault();
return;
}
if (add) {
this.pressedKeys.add(event.code);
} else {
this.pressedKeys.delete(event.code);
}
this.updateState();
}
clearKeys() {
this.pressedKeys.clear();
}
checkKeysMatch(targetKeys) {
if (!targetKeys.length) return false;
return targetKeys.every((key) => {
const normalizedKey = key === "Shift" ? "ShiftLeft" : key;
return this.pressedKeys.has(key) || this.pressedKeys.has(normalizedKey);
});
}
handleHoverEvent(event) {
if (!this.isInspectorMode) return;
const target = findWebWidgetContainer(event.target);
if (this.hoveredElement !== target) {
if (this.hoveredElement) {
this.hoveredElement.style.removeProperty("cursor");
}
this.hoveredElement = target;
this.updateState();
}
}
handleClickEvent(event) {
if (this.isInspectorMode) {
const toolbar = this.renderRoot?.querySelector(".wwi-toolbar");
const isToolbarClick = toolbar?.contains(event.target);
if (isToolbarClick) {
return;
}
const widgetTarget = findWebWidgetContainer(event.target);
if (widgetTarget) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
const inspectUrl = widgetTarget?.getAttribute("import");
if (inspectUrl) {
document.body.style.cursor = "progress";
this.openInEditor(inspectUrl, this.dir);
this.updateState();
}
}
}
}
updateState() {
const targetKeys = this.keys;
if (!this.isToolbarVisible && this.checkKeysMatch(targetKeys)) {
this.showToolbar();
}
if (this.isInspectorMode && this.hoveredElement) {
this.updateInspectorOverlays(this.hoveredElement);
} else {
this.clearInspectorOverlays();
}
this.setButtonSelected(this.isInspectorMode);
if (this.isInspectorMode) {
if (this.hoveredElement) {
this.hoveredElement.style.cursor = "pointer";
}
} else {
document.body.style.removeProperty("cursor");
const allElements = document.querySelectorAll("*");
allElements.forEach((el) => {
if (el instanceof HTMLElement) {
el.style.removeProperty("cursor");
}
});
}
this.requestUpdate();
}
createOverlayElement() {
if (this.overlayElement) {
return this.overlayElement;
}
if (!this.isInspectorMode) {
const dummy = document.createElement("div");
dummy.style.visibility = "hidden";
return dummy;
}
this.ensureOverlayStyles();
const overlay = document.createElement("div");
overlay.className = "wwi-overlay";
overlay.setAttribute("aria-hidden", "true");
document.body.appendChild(overlay);
this.overlayElement = overlay;
return overlay;
}
showOverlay(element) {
if (!this.isInspectorMode || !element) return;
const overlay = this.createOverlayElement();
const rect = this.currentElementBounds;
overlay.style.top = `${rect.top}px`;
overlay.style.left = `${rect.left}px`;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;
overlay.style.visibility = "visible";
}
cleanupOverlay() {
if (this.overlayElement) {
this.overlayElement.remove();
this.overlayElement = null;
}
}
setButtonSelected(selected) {
const btn = this.renderRoot?.querySelector(".wwi-inspect-btn");
if (btn) {
btn.setAttribute("data-selected", selected ? "true" : "false");
btn.setAttribute("aria-pressed", selected ? "true" : "false");
}
}
updateWidgetCount() {
this.widgetCount = document.querySelectorAll("web-widget[import]").length;
}
createTooltipElement() {
if (this.tooltipElement) {
return this.tooltipElement;
}
if (!this.isInspectorMode) {
const dummy = document.createElement("div");
dummy.style.visibility = "hidden";
return dummy;
}
this.ensureTooltipStyles();
const tooltip = document.createElement("div");
tooltip.className = "wwi-tooltip";
tooltip.setAttribute("aria-hidden", "true");
document.body.appendChild(tooltip);
this.tooltipElement = tooltip;
return tooltip;
}
ensureOverlayStyles() {
if (!this.isInspectorMode) {
return;
}
if (document.getElementById("wwi-overlay-styles")) {
return;
}
const style = document.createElement("style");
style.id = "wwi-overlay-styles";
style.textContent = `
.wwi-overlay {
position: fixed;
z-index: ${DESIGN_SYSTEM.zIndex.OVERLAY};
pointer-events: none;
visibility: hidden;
opacity: 1;
display: block;
background: rgba(34, 197, 94, 0.3);
border: 2px solid var(--wwi-overlay-border, #22c55e);
border-radius: 0;
box-sizing: border-box;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
`;
document.head.appendChild(style);
}
ensureTooltipStyles() {
if (!this.isInspectorMode) {
return;
}
if (document.getElementById("wwi-tooltip-styles")) {
return;
}
const style = document.createElement("style");
style.id = "wwi-tooltip-styles";
style.textContent = `
.wwi-tooltip {
position: fixed;
z-index: ${DESIGN_SYSTEM.zIndex.TOOLTIP};
pointer-events: none;
background: var(--wwi-bg, rgba(255, 255, 255, 0.95));
color: var(--wwi-fg, #222);
border: 1px solid var(--wwi-border, #e5e7eb);
border-radius: 0.375rem;
padding: ${DESIGN_SYSTEM.spacing.lg} ${DESIGN_SYSTEM.spacing.xl};
font-size: ${DESIGN_SYSTEM.typography.fontSize.base};
font-family: ${DESIGN_SYSTEM.typography.fontFamily.sans};
line-height: ${DESIGN_SYSTEM.typography.lineHeight.normal};
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.normal};
letter-spacing: ${DESIGN_SYSTEM.typography.letterSpacing.normal};
word-wrap: break-word;
white-space: pre-wrap;
box-shadow: ${DESIGN_SYSTEM.boxShadow.base};
backdrop-filter: blur(8px);
max-width: 640px;
min-width: 280px;
visibility: hidden;
display: block;
}
.wwi-tooltip-table {
width: 100%;
border-collapse: collapse;
font-size: ${DESIGN_SYSTEM.typography.fontSize.xs};
line-height: ${DESIGN_SYSTEM.typography.lineHeight.normal};
}
.wwi-tooltip-table td {
padding: ${DESIGN_SYSTEM.spacing.xs} 0;
vertical-align: top;
}
.wwi-tooltip-table .key {
color: var(--wwi-accent, #888);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.medium};
padding-right: ${DESIGN_SYSTEM.spacing.lg};
white-space: nowrap;
text-align: left;
opacity: 0.8;
}
.wwi-tooltip-table .value {
color: var(--wwi-fg, #222);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.normal};
word-break: break-all;
text-align: right;
}
.wwi-tooltip-table tr.priority-high .key {
color: var(--wwi-fg, #222);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.semibold};
opacity: 1;
}
.wwi-tooltip-table tr.priority-high .value {
color: var(--wwi-fg, #222);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.medium};
}
.wwi-tooltip-table tr.priority-medium .key {
color: var(--wwi-accent, #666);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.medium};
opacity: 0.9;
}
.wwi-tooltip-table tr.priority-medium .value {
color: var(--wwi-fg, #444);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.normal};
}
.wwi-tooltip-table tr.priority-low .key {
color: var(--wwi-accent, #888);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.normal};
opacity: 0.7;
}
.wwi-tooltip-table tr.priority-low .value {
color: var(--wwi-fg, #666);
font-weight: ${DESIGN_SYSTEM.typography.fontWeight.normal};
opacity: 0.8;
}
.wwi-tooltip-help {
margin-top: ${DESIGN_SYSTEM.spacing.md};
padding-top: ${DESIGN_SYSTEM.spacing.md};
border-top: 1px solid var(--wwi-border, #e5e7eb);
font-size: ${DESIGN_SYSTEM.typography.fontSize.xs};
color: var(--wwi-accent, #888);
text-align: center;
opacity: 0.8;
}
@media (max-width: 768px) {
.inspector-toolbar {
bottom: ${DESIGN_SYSTEM.spacing.md};
left: ${DESIGN_SYSTEM.spacing.md};
right: ${DESIGN_SYSTEM.spacing.md};
flex-wrap: wrap;
max-width: calc(100vw - ${DESIGN_SYSTEM.spacing.xl});
padding: ${DESIGN_SYSTEM.spacing.md} ${DESIGN_SYSTEM.spacing.lg};
gap: ${DESIGN_SYSTEM.spacing.md};
}
}
`;
document.head.appendChild(style);
}
calculateSimplePosition(elementRect, tooltipRect) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const padding = 8;
const minMargin = 8;
let x = elementRect.left;
let y = elementRect.top - tooltipRect.height - padding;
if (y < minMargin) {
y = elementRect.top + elementRect.height + padding;
}
x = Math.max(
minMargin,
Math.min(viewportWidth - tooltipRect.width - minMargin, x)
);
y = Math.max(
minMargin,
Math.min(viewportHeight - tooltipRect.height - minMargin, y)
);
return { x, y };
}
generateTooltipContent(element) {
const debugData = collectElementData(element);
const formattedData = formatDebugData(debugData, element);
const tableRows = formattedData.map((item) => {
const priorityClass = getPriorityClass(item.priority);
return `<tr class="${priorityClass}"><td class="key">${item.key}</td><td class="value">${item.value}</td></tr>`;
}).join("");
const helpText = debugData.isWebWidget ? '<div class="wwi-tooltip-help">\u{1F4A1} Click to open source code</div>' : "";
return `<table class="wwi-tooltip-table">${tableRows}</table>${helpText}`;
}
showTooltip(element) {
if (!this.isInspectorMode || !element) return;
const tooltip = this.createTooltipElement();
const content = this.generateTooltipContent(element);
tooltip.innerHTML = content;
tooltip.style.visibility = "hidden";
tooltip.style.display = "block";
tooltip.offsetHeight;
const elementRect = this.currentElementBounds;
const tooltipRect = tooltip.getBoundingClientRect();
const position = this.calculateSimplePosition(elementRect, tooltipRect);
tooltip.style.left = `${position.x}px`;
tooltip.style.top = `${position.y}px`;
tooltip.style.visibility = "visible";
}
hideTooltip() {
if (this.tooltipElement) {
this.tooltipElement.style.visibility = "hidden";
}
}
cleanupTooltip() {
if (this.tooltipElement) {
this.tooltipElement.remove();
this.tooltipElement = null;
}
}
updateTooltipContent() {
if (this.hoveredElement && this.tooltipElement) {
const content = this.generateTooltipContent(this.hoveredElement);
this.tooltipElement.innerHTML = content;
}
}
cleanupInspectorElements() {
if (this.overlayElement) {
this.overlayElement.remove();
this.overlayElement = null;
}
const overlayStyles = document.getElementById("wwi-overlay-styles");
if (overlayStyles) {
overlayStyles.remove();
}
const tooltipStyles = document.getElementById("wwi-tooltip-styles");
if (tooltipStyles) {
tooltipStyles.remove();
}
if (this.tooltipElement) {
this.tooltipElement.remove();
this.tooltipElement = null;
}
document.body.style.removeProperty("cursor");
}
toggleInspector() {
this.isInspectorMode = !this.isInspectorMode;
if (!this.isInspectorMode) {
this.cleanupInspectorElements();
}
this.updateState();
}
showToolbar() {
this.isToolbarVisible = true;
this.updateState();
setStoredValue("web-widget-inspector-visible", true);
}
hideToolbar() {
this.isToolbarVisible = false;
this.updateState();
setStoredValue("web-widget-inspector-visible", false);
}
minimizeToolbar() {
this.isMinimized = !this.isMinimized;
this.updateState();
setStoredValue("web-widget-inspector-minimized", this.isMinimized);
}
openInEditor(path, srcDir) {
if (srcDir) {
const resolvedURL = new URL(path, document.baseURI);
if (resolvedURL.origin === location.origin) {
const filePath = srcDir + resolvedURL.pathname;
this.openFileInEditor(filePath);
} else {
location.href = resolvedURL.href;
}
} else {
this.openFileInEditor(path);
}
}
openFileInEditor(filePath) {
document.body.style.cursor = "progress";
fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`).finally(
() => {
document.body.style.removeProperty("cursor");
}
);
}
getCurrentFilePath() {
if (!this.pageSource) return "";
try {
if (this.pageSource.startsWith("source://")) {
return this.pageSource.replace("source://", "");
}
const url = new URL(this.pageSource);
return url.pathname;
} catch {
return this.pageSource;
}
}
loadRouteModuleSourceFromHeaders() {
if (!this.pageSource) {
fetch(window.location.href, { method: "HEAD" }).then((response) => {
const moduleSource = response.headers.get("x-module-source");
if (moduleSource) {
this.pageSource = moduleSource;
}
}).catch((error) => {
console.error(
"Error loading route module source from headers:",
error
);
});
}
}
/**
* Batch update all inspector overlays for the current hovered element
* This avoids multiple expensive getElementBox calls
*/
updateInspectorOverlays(element) {
const bounds = getElementBox(element);
this.currentElementBounds = bounds ?? {
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 0,
height: 0
};
this.showOverlay(element);
this.showTooltip(element);
}
clearInspectorOverlays() {
this.currentElementBounds = {
top: 0,
left: 0,
right: 0,
bottom: 0,
width: 0,
height: 0
};
this.cleanupOverlay();
this.cleanupTooltip();
}
render() {
if (!this.isToolbarVisible) {
return html``;
}
return html`
<div
class="wwi-toolbar"
data-minimized=${this.isMinimized ? "true" : "false"}
data-inspector-active=${this.isInspectorMode ? "true" : "false"}>
<div class="wwi-header">
<div
class="wwi-logo"
tabindex="0"
title="Web Widget Inspector"
@click=${this.minimizeToolbar}>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true">
<path
d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2Z" />
</svg>
<div class="logo-text">
<span class="logo-title">WEB WIDGET</span>
<span class="logo-subtitle">DEV INSPECTOR</span>
</div>
</div>
</div>
<div class="wwi-info">
<span class="widget-count">${this.widgetCount} widgets</span>
</div>
<div class="wwi-actions">
<button
class="wwi-btn wwi-inspect-btn"
@click=${this.toggleInspector}
title="Click to activate widget inspector (ESC to exit)"
aria-label="Activate widget inspector mode">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<span>Inspect</span>
</button>
<button
class="wwi-btn wwi-page-btn"
@click=${() => {
const currentFilePath = this.getCurrentFilePath();
if (currentFilePath) {
this.openInEditor(currentFilePath);
}
}}
title="Open current page source code"
aria-label="Open current page source code">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M9 9h6v6H9z" />
</svg>
<span>Open Source</span>
</button>
</div>
<div class="wwi-controls">
<button
class="wwi-btn wwi-hide-btn"
@click=${this.hideToolbar}
title="Hide Web Widget Inspector"
aria-label="Hide inspector toolbar">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
<button
class="wwi-btn wwi-toggle-btn"
@click=${this.minimizeToolbar}
title="Minimize Web Widget Inspector"
aria-label="Minimize inspector toolbar">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
</div>
</div>
`;
}
};
HTMLWebWidgetInspectorElement.styles = css`
:host {
display: contents;
}
.wwi-toolbar,
.wwi-toolbar *,
.wwi-tooltip,
.wwi-tooltip * {
font-family: var(
--wwi-font-sans,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif
);
}
.wwi-toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
position: fixed;
bottom: 1rem;
left: 1rem;
z-index: ${DESIGN_SYSTEM.zIndex.TOOLBAR};
background: var(--wwi-bg, rgba(255, 255, 255, 0.95));
color: var(--wwi-fg, #222);
padding: 0.75rem 1rem;
border-radius: 0.375rem;
border: 1px solid var(--wwi-border, #e5e7eb);
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
font-family: var(
--wwi-font-sans,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif
);
font-size: 0.75rem;
min-width: max-content;
transition: all 0.2s;
backdrop-filter: blur(8px);
}
.wwi-toolbar[data-minimized='true'] {
padding: 0;
gap: 0;
min-width: 0;
width: 32px;
height: 32px;
background: var(--wwi-bg, rgba(255, 255, 255, 0.95));
border: 1px solid var(--wwi-border, #e5e7eb);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 50%;
cursor: pointer;
justify-content: center;
align-items: center;
display: flex;
transition:
background 0.2s,
border 0.2s;
}
.wwi-toolbar[data-minimized='true']:hover {
background: var(--wwi-bg, rgba(255, 255, 255, 0.98));
border: 1px solid var(--wwi-border, #e5e7eb);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.wwi-toolbar[data-minimized='true'] .wwi-header {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
height: 100%;
}
.wwi-toolbar[data-minimized='true'] .wwi-logo {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.wwi-toolbar[data-minimized='true'] .wwi-logo svg {
margin: 0;
width: 16px;
height: 16px;
display: block;
opacity: 0.7;
filter: none;
transition: opacity 0.2s;
}
.wwi-toolbar[data-minimized='true']:hover .wwi-logo svg {
opacity: 1;
filter: none;
}
.wwi-toolbar[data-minimized='true'] .wwi-header .wwi-logo span,
.wwi-toolbar[data-minimized='true'] .wwi-logo .logo-text,
.wwi-toolbar[data-minimized='true'] .wwi-info,
.wwi-toolbar[data-minimized='true'] .wwi-actions,
.wwi-toolbar[data-minimized='true'] .wwi-controls {
display: none;
}
.wwi-overlay {
position: fixed;
z-index: ${DESIGN_SYSTEM.zIndex.OVERLAY};
pointer-events: none;
background: repeating-linear-gradient(
45deg,
rgba(102, 126, 234, 0.5),
rgba(102, 126, 234, 0.5) 5px,
rgba(255, 255, 255, 0.5) 5px,
rgba(255, 255, 255, 0.5) 10px
);
border: 2px solid rgba(102, 126, 234, 0.9);
border-radius: 0;
box-sizing: border-box;
}
.wwi-tooltip {
/* Styles dynamically injected by ensureTooltipStyles */
visibility: hidden;
}
.wwi-header {
display: flex;
align-items: center;
gap: 0.375rem;
padding-right: 0.5rem;
}
.wwi-logo {
display: flex;
align-items: center;
gap: 0.375rem;
font-weight: 600;
color: var(--wwi-accent, #888);
font-size: 0.75rem;
user-select: none;
}
.wwi-logo svg {
cursor: pointer;
color: var(--wwi-accent, #888);
width: 16px;
height: 16px;
margin-right: 0.125rem;
}
.logo-text {
display: flex;
flex-direction: column;
line-height: 1;
gap: 0.125rem;
align-items: flex-start;
}
.logo-title {
font-weight: 700;
font-size: 0.7rem;
color: var(--wwi-accent, #888);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.logo-subtitle {
font-weight: 500;
font-size: 0.5rem;
color: var(--wwi-accent, #888);
opacity: 0.75;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.wwi-info {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--wwi-fg, #222);
opacity: 0.7;
}
.widget-count {
color: var(--wwi-accent, #888);
font-weight: 500;
}
.wwi-actions {
display: flex;
align-items: center;
gap: 0.375rem;
}
.wwi-controls {
display: flex;
align-items: center;
gap: 0.125rem;
}
.wwi-btn {
display: flex;
align-items: center;
gap: 0.25rem;
background: transparent;
border: 1px solid transparent;
color: var(--wwi-fg, #222);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.15s ease;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
opacity: 0.8;
}
.wwi-btn:hover {
background: rgba(0, 0, 0, 0.12);
border-color: rgba(0, 0, 0, 0.2);
color: var(--wwi-fg, #111);
opacity: 1;
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.wwi-btn[data-selected='true'] {
background: rgba(34, 197, 94, 0.15);
border-color: #22c55e;
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
opacity: 1;
position: relative;
}
.wwi-btn svg {
flex-shrink: 0;
width: 14px;
height: 14px;
stroke: currentColor;
opacity: 0.9;
}
.wwi-controls .wwi-btn {
padding: 0.25rem;
border-radius: 50%;
}
.wwi-toolbar[data-inspector-active='true'] {
background: var(--wwi-bg, rgba(255, 255, 255, 0.98));
}
@media (max-width: 768px) {
.wwi-toolbar {
bottom: 0.75rem;
left: 0.75rem;
right: 0.75rem;
flex-wrap: wrap;
max-width: calc(100vw - 1.25rem);
}
}
`;
__decorateClass([
property({ type: String })
], HTMLWebWidgetInspectorElement.prototype, "dir", 2);
__decorateClass([
property({ type: Array })
], HTMLWebWidgetInspectorElement.prototype, "keys", 2);
__decorateClass([
property({ type: String, attribute: "page-source" })
], HTMLWebWidgetInspectorElement.prototype, "pageSource", 2);
__decorateClass([
property({ type: String })
], HTMLWebWidgetInspectorElement.prototype, "theme", 1);
__decorateClass([
state()
], HTMLWebWidgetInspectorElement.prototype, "isToolbarVisible", 2);
__decorateClass([
state()
], HTMLWebWidgetInspectorElement.prototype, "isInspectorMode", 2);
__decorateClass([
state()
], HTMLWebWidgetInspectorElement.prototype, "isMinimized", 2);
__decorateClass([
state()
], HTMLWebWidgetInspectorElement.prototype, "hoveredElement", 2);
__decorateClass([
state()
], HTMLWebWidgetInspectorElement.prototype, "widgetCount", 2);
export { HTMLWebWidgetInspectorElement };
//# sourceMappingURL=element.js.map
//# sourceMappingURL=element.js.map