329 lines
9.1 KiB
JavaScript
329 lines
9.1 KiB
JavaScript
(() => {
|
|
function create({
|
|
IDS,
|
|
CLASSES,
|
|
USER_BUTTON_LABEL,
|
|
normalizeText,
|
|
isVisible,
|
|
}) {
|
|
function applyDialogTheme(overlay, theme) {
|
|
const dialog = overlay?.querySelector(`#${IDS.dialog}`);
|
|
|
|
if (!dialog) {
|
|
return;
|
|
}
|
|
|
|
dialog.setAttribute("data-theme", theme);
|
|
}
|
|
|
|
function ensureStyles() {
|
|
// Styles are loaded by manifest.json. This function remains as a stable callsite.
|
|
}
|
|
|
|
function applyTheme(header, functionsContainer, anchor, mount, overlay) {
|
|
const headerStyle = window.getComputedStyle(header);
|
|
const functionsStyle = window.getComputedStyle(functionsContainer);
|
|
const sourceBackground = resolveBackgroundColor(header, headerStyle);
|
|
const sourceForeground = resolveForegroundColor(
|
|
header,
|
|
functionsContainer,
|
|
anchor,
|
|
headerStyle,
|
|
functionsStyle
|
|
);
|
|
const workbenchHeader = mix(
|
|
sourceBackground,
|
|
{ r: 75, g: 51, b: 93, a: 1 },
|
|
0.52
|
|
);
|
|
const workbenchAccent = mix(
|
|
workbenchHeader,
|
|
{ r: 108, g: 77, b: 128, a: 1 },
|
|
0.38
|
|
);
|
|
const focusColor = mix(sourceForeground, workbenchHeader, 0.28);
|
|
const themeTargets = [mount, overlay].filter(Boolean);
|
|
|
|
for (const target of themeTargets) {
|
|
target.style.setProperty(
|
|
"--arch-panel-bg",
|
|
toColor(mix(sourceBackground, sourceForeground, 0.12))
|
|
);
|
|
target.style.setProperty(
|
|
"--arch-panel-bg-hover",
|
|
toColor(mix(sourceBackground, sourceForeground, 0.22))
|
|
);
|
|
target.style.setProperty(
|
|
"--arch-panel-border",
|
|
toColor(mix(sourceBackground, sourceForeground, 0.32))
|
|
);
|
|
target.style.setProperty("--arch-panel-fg", toColor(sourceForeground, 1));
|
|
target.style.setProperty("--arch-panel-focus", toColor(focusColor, 0.94));
|
|
target.style.setProperty(
|
|
"--arch-panel-font",
|
|
functionsStyle.fontFamily || headerStyle.fontFamily || "inherit"
|
|
);
|
|
target.style.setProperty(
|
|
"--arch-workbench-header",
|
|
toColor(workbenchHeader, 1)
|
|
);
|
|
target.style.setProperty(
|
|
"--arch-workbench-accent",
|
|
toColor(workbenchAccent, 1)
|
|
);
|
|
}
|
|
}
|
|
|
|
function resolveForegroundColor(
|
|
header,
|
|
functionsContainer,
|
|
anchor,
|
|
headerStyle,
|
|
functionsStyle
|
|
) {
|
|
const salesPlanningElement = findTextElement(header, "Sales Planning");
|
|
const salesPlanningColor = salesPlanningElement
|
|
? parseColor(window.getComputedStyle(salesPlanningElement).color)
|
|
: null;
|
|
const functionsItemColor = findVisibleChildColor(functionsContainer);
|
|
const userColor = anchor
|
|
? parseColor(window.getComputedStyle(anchor).color)
|
|
: null;
|
|
|
|
return (
|
|
userColor ||
|
|
functionsItemColor ||
|
|
parseColor(functionsStyle.color) ||
|
|
salesPlanningColor ||
|
|
parseColor(headerStyle.color) || {
|
|
r: 255,
|
|
g: 255,
|
|
b: 255,
|
|
a: 1,
|
|
}
|
|
);
|
|
}
|
|
|
|
function resolveBackgroundColor(element, style) {
|
|
const directBackground = parseColor(style.backgroundColor);
|
|
|
|
if (directBackground && directBackground.a > 0) {
|
|
return directBackground;
|
|
}
|
|
|
|
let current = element.parentElement;
|
|
|
|
while (current) {
|
|
const currentBackground = parseColor(
|
|
window.getComputedStyle(current).backgroundColor
|
|
);
|
|
|
|
if (currentBackground && currentBackground.a > 0) {
|
|
return currentBackground;
|
|
}
|
|
|
|
current = current.parentElement;
|
|
}
|
|
|
|
return { r: 75, g: 51, b: 93, a: 1 };
|
|
}
|
|
|
|
function parseColor(value) {
|
|
if (!value || value === "transparent") {
|
|
return null;
|
|
}
|
|
|
|
const rgbMatch = value.match(
|
|
/rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,/]+([.\d]+))?\s*\)/i
|
|
);
|
|
|
|
if (rgbMatch) {
|
|
return {
|
|
r: Number.parseInt(rgbMatch[1], 10),
|
|
g: Number.parseInt(rgbMatch[2], 10),
|
|
b: Number.parseInt(rgbMatch[3], 10),
|
|
a: rgbMatch[4] === undefined ? 1 : Number.parseFloat(rgbMatch[4]),
|
|
};
|
|
}
|
|
|
|
const hexMatch = value.match(/^#([\da-f]{3,8})$/i);
|
|
|
|
if (!hexMatch) {
|
|
return null;
|
|
}
|
|
|
|
const hexValue = hexMatch[1];
|
|
|
|
if (hexValue.length === 3 || hexValue.length === 4) {
|
|
const [r, g, b, a = "f"] = hexValue.split("");
|
|
|
|
return {
|
|
r: Number.parseInt(r + r, 16),
|
|
g: Number.parseInt(g + g, 16),
|
|
b: Number.parseInt(b + b, 16),
|
|
a: Number.parseInt(a + a, 16) / 255,
|
|
};
|
|
}
|
|
|
|
if (hexValue.length === 6 || hexValue.length === 8) {
|
|
return {
|
|
r: Number.parseInt(hexValue.slice(0, 2), 16),
|
|
g: Number.parseInt(hexValue.slice(2, 4), 16),
|
|
b: Number.parseInt(hexValue.slice(4, 6), 16),
|
|
a:
|
|
hexValue.length === 8
|
|
? Number.parseInt(hexValue.slice(6, 8), 16) / 255
|
|
: 1,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function mix(base, tint, amount) {
|
|
const ratio = clamp(amount, 0, 1);
|
|
|
|
return {
|
|
r: Math.round(base.r + (tint.r - base.r) * ratio),
|
|
g: Math.round(base.g + (tint.g - base.g) * ratio),
|
|
b: Math.round(base.b + (tint.b - base.b) * ratio),
|
|
a: base.a + (tint.a - base.a) * ratio,
|
|
};
|
|
}
|
|
|
|
function toColor(color, alphaOverride) {
|
|
const alpha = alphaOverride ?? color.a ?? 1;
|
|
|
|
return `rgba(${color.r}, ${color.g}, ${color.b}, ${clamp(alpha, 0, 1)})`;
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.min(Math.max(value, min), max);
|
|
}
|
|
|
|
function findUserAnchor(container) {
|
|
const selector = [
|
|
`button[label="${USER_BUTTON_LABEL}"]`,
|
|
`[role="button"][label="${USER_BUTTON_LABEL}"]`,
|
|
`oj-button[label="${USER_BUTTON_LABEL}"]`,
|
|
`button[aria-label="${USER_BUTTON_LABEL}"]`,
|
|
`[role="button"][aria-label="${USER_BUTTON_LABEL}"]`,
|
|
`[label="${USER_BUTTON_LABEL}"]`,
|
|
`[aria-label="${USER_BUTTON_LABEL}"]`,
|
|
].join(", ");
|
|
|
|
const directMatch = container.querySelector(selector);
|
|
|
|
if (directMatch && isVisible(directMatch)) {
|
|
return directMatch;
|
|
}
|
|
|
|
const fallbackMatch = Array.from(container.querySelectorAll("*")).find(
|
|
(element) => {
|
|
if (!isVisible(element) || element.closest(`#${IDS.mount}`)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
element.getAttribute("label") === USER_BUTTON_LABEL ||
|
|
element.getAttribute("aria-label") === USER_BUTTON_LABEL
|
|
);
|
|
}
|
|
);
|
|
|
|
return fallbackMatch || null;
|
|
}
|
|
|
|
function findTextElement(container, expectedText) {
|
|
const normalizedExpected = normalizeText(expectedText);
|
|
const elements = Array.from(container.querySelectorAll("*"));
|
|
|
|
return (
|
|
elements.find((element) => {
|
|
const text = normalizeText(element.textContent || "");
|
|
|
|
return text && text.includes(normalizedExpected);
|
|
}) || null
|
|
);
|
|
}
|
|
|
|
function resolveInsertionTarget(container, anchor, mount) {
|
|
const directAnchor = anchor ? getDirectChild(anchor, container) : null;
|
|
const fallbackTarget = findFirstInsertionTarget(container, mount);
|
|
const candidate = directAnchor || fallbackTarget;
|
|
|
|
if (!candidate) {
|
|
return null;
|
|
}
|
|
|
|
if (candidate === mount) {
|
|
return findNextValidSibling(container, mount);
|
|
}
|
|
|
|
return candidate.parentElement === container ? candidate : null;
|
|
}
|
|
|
|
function getDirectChild(element, container) {
|
|
let current = element;
|
|
|
|
while (current && current.parentElement !== container) {
|
|
current = current.parentElement;
|
|
}
|
|
|
|
return current && current.parentElement === container ? current : null;
|
|
}
|
|
|
|
function findFirstInsertionTarget(container, mount) {
|
|
return Array.from(container.children).find((child) => child !== mount) || null;
|
|
}
|
|
|
|
function findNextValidSibling(container, mount) {
|
|
let sibling = mount.nextElementSibling;
|
|
|
|
while (sibling) {
|
|
if (sibling.parentElement === container) {
|
|
return sibling;
|
|
}
|
|
|
|
sibling = sibling.nextElementSibling;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findVisibleChildColor(container) {
|
|
const visibleChild = Array.from(container.querySelectorAll("*")).find(
|
|
(element) => isVisible(element) && !element.closest(`#${IDS.mount}`)
|
|
);
|
|
|
|
return visibleChild
|
|
? parseColor(window.getComputedStyle(visibleChild).color)
|
|
: null;
|
|
}
|
|
|
|
function lockScroll() {
|
|
document.documentElement.classList.add(CLASSES.modalOpen);
|
|
document.body?.classList.add(CLASSES.modalOpen);
|
|
}
|
|
|
|
function unlockScroll() {
|
|
document.documentElement.classList.remove(CLASSES.modalOpen);
|
|
document.body?.classList.remove(CLASSES.modalOpen);
|
|
}
|
|
|
|
return Object.freeze({
|
|
applyDialogTheme,
|
|
ensureStyles,
|
|
applyTheme,
|
|
findUserAnchor,
|
|
resolveInsertionTarget,
|
|
lockScroll,
|
|
unlockScroll,
|
|
});
|
|
}
|
|
|
|
globalThis.ArchPanelShellView = Object.freeze({
|
|
create,
|
|
});
|
|
})();
|