DSH-Arch-Setup/plugins/dsh-header-menu/lib/client.js
Шурупов Илья Викторович cf9443753e Add one-command Arch DSH setup
Package the portable DSH profiles, local plugins, desktop integration, Firefox wrapper, and ownership-aware installer lifecycle. Bundle checksum-pinned OpenVSCode, editor extensions, translation assets, and cloudflared so unreliable upstream artifact downloads cannot break a clean installation.
2026-09-12 19:13:36 +03:00

192 lines
7.8 KiB
JavaScript

/**
* dsh-header-menu — browser half.
*
* The conversation header exposes a utilities region (`.headerUtilities` inside
* `.headerActions`) that plugins/packages fill via the
* `conversation.session.header.utilities` slot: Session log, Stop All, Resume
* Stopped, Resume All, the Git indicator, etc. Several standalone pills eat a
* lot of horizontal space. This plugin collapses them behind one `⋯` trigger.
*
* Strategy — no React fighting:
* • We never move or synthetically click the real buttons; their real React
* handlers stay intact. We only (a) inject global CSS that restyles the
* `.headerUtilities` container into a hidden dropdown panel, and (b) add a
* single vanilla trigger button next to it.
* • CSS module classes are content-hashed, so we match the stable suffix via
* `[class*="_headerUtilities"]` / `[class*="_headerActions"]`.
* • Open/close state and the "managed" marks are stored in imperatively-set
* `data-*` attributes, which React does not manage and therefore does not
* clobber on re-render. A MutationObserver re-applies the enhancement (and
* re-adds the trigger) whenever React rebuilds the header.
* @module dsh-header-menu/client
*/
window.__ModuleLoader__.load({
id: "dsh-header-menu",
factory: (require) => {
var module = { exports: {} };
var exports = module.exports;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
// ---- CONFIG (edit here) -----------------------------------------------
// Label shown on the collapsed trigger button.
const TRIGGER_LABEL = "⋯"; // ⋯ (e.g. "Menu", "Actions ▾")
// Close the dropdown after clicking one of its items.
const CLOSE_ON_ITEM_CLICK = true;
// -----------------------------------------------------------------------
const STYLE_ID = "dsh-header-menu-style";
const STYLE_TEXT = [
// host anchors the absolutely-positioned panel
'[data-dsh-hdr-menu="host"]{position:relative}',
// the utility row becomes a dropdown panel
'[data-dsh-hdr-menu="panel"]{',
"position:absolute;top:calc(100% + 8px);right:0;z-index:2147483000;",
"margin-left:0 !important;flex-direction:column !important;",
"align-items:stretch !important;gap:6px !important;padding:8px;min-width:210px;",
"background:var(--dsw-alias-bg-elevated, var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-base,#1f1f1f)));",
"border:1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12));",
"border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.45)}",
// hidden unless the host is marked open
'[data-dsh-hdr-menu="host"]:not([data-open="1"]) [data-dsh-hdr-menu="panel"]{display:none !important}',
// stack items full width, left aligned
'[data-dsh-hdr-menu="panel"] > *{width:100%;box-sizing:border-box;justify-content:flex-start !important}',
// known grouped entry (fleet-control) stacks vertically too
'[data-dsh-hdr-menu="panel"] .dsh-fleet-control{flex-direction:column;align-items:stretch;gap:6px;width:100%}',
'[data-dsh-hdr-menu="panel"] .dsh-fleet-control > *{width:100%;justify-content:flex-start}',
// the trigger pill
".dsh-hdr-menu-trigger{border:1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12));",
"height:32px;min-width:32px;color:var(--dsw-alias-label-primary);background:transparent;",
"border-radius:18px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;",
"gap:5px;padding:6px 12px;font-size:15px;line-height:20px;white-space:nowrap;",
"font-family:var(--dsw-font-family)}",
".dsh-hdr-menu-trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}",
'.dsh-hdr-menu-trigger[aria-expanded="true"]{background:var(--dsw-alias-interactive-bg-hover)}',
].join("");
function installStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.dataset.plugin = "dsh-header-menu";
style.textContent = STYLE_TEXT;
(document.head || document.documentElement).appendChild(style);
}
/** Close every open header menu. */
function closeAll(except) {
document.querySelectorAll('[data-dsh-hdr-menu="host"][data-open="1"]').forEach((host) => {
if (host === except) return;
host.removeAttribute("data-open");
const t = host.querySelector(".dsh-hdr-menu-trigger");
if (t) t.setAttribute("aria-expanded", "false");
});
}
/** Build the trigger button for a given host (the .headerActions element). */
function makeTrigger(host) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "dsh-hdr-menu-trigger";
btn.textContent = TRIGGER_LABEL;
btn.title = "Session actions";
btn.setAttribute("aria-label", "Session actions menu");
btn.setAttribute("aria-expanded", "false");
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const open = host.getAttribute("data-open") === "1";
closeAll(host);
if (open) {
host.removeAttribute("data-open");
btn.setAttribute("aria-expanded", "false");
} else {
host.setAttribute("data-open", "1");
btn.setAttribute("aria-expanded", "true");
}
});
return btn;
}
/** Enhance every header-utilities region currently in the DOM (idempotent). */
function enhance() {
const panels = document.querySelectorAll('[class*="_headerUtilities"]');
panels.forEach((panel) => {
const host = panel.parentElement;
if (!host) return;
panel.setAttribute("data-dsh-hdr-menu", "panel");
host.setAttribute("data-dsh-hdr-menu", "host");
// close-on-item-click wiring (once)
if (CLOSE_ON_ITEM_CLICK && panel.getAttribute("data-dsh-hdr-wired") !== "1") {
panel.setAttribute("data-dsh-hdr-wired", "1");
panel.addEventListener("click", (e) => {
if (e.target && e.target.closest("button,a,[role='menuitem'],[role='button']")) {
// let the real handler run, then close on next tick
setTimeout(() => closeAll(), 0);
}
});
}
// ensure exactly one trigger, placed right after the panel
if (!host.querySelector(".dsh-hdr-menu-trigger")) {
const trigger = makeTrigger(host);
if (panel.nextSibling) host.insertBefore(trigger, panel.nextSibling);
else host.appendChild(trigger);
}
});
}
let scheduled = false;
function scheduleEnhance() {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
enhance();
});
}
/** No client services required. */
const inject = [];
function apply(ctx) {
const start = () => {
installStyle();
enhance();
const observer = new MutationObserver(scheduleEnhance);
observer.observe(document.body, { childList: true, subtree: true });
const onDocClick = (e) => {
const host = e.target && e.target.closest && e.target.closest('[data-dsh-hdr-menu="host"]');
if (!host) closeAll();
};
const onKey = (e) => {
if (e.key === "Escape") closeAll();
};
document.addEventListener("click", onDocClick, true);
document.addEventListener("keydown", onKey, true);
return () => {
observer.disconnect();
document.removeEventListener("click", onDocClick, true);
document.removeEventListener("keydown", onKey, true);
closeAll();
const style = document.getElementById(STYLE_ID);
if (style) style.remove();
document.querySelectorAll(".dsh-hdr-menu-trigger").forEach((n) => n.remove());
document.querySelectorAll('[data-dsh-hdr-menu]').forEach((n) => {
n.removeAttribute("data-dsh-hdr-menu");
n.removeAttribute("data-open");
n.removeAttribute("data-dsh-hdr-wired");
});
};
};
if (ctx && typeof ctx.effect === "function") ctx.effect(start, "dsh-header-menu: collapse header utilities");
else start();
}
exports.apply = apply;
exports.inject = inject;
return module.exports;
},
});