Greasy Fork

Greasy Fork is available in English.

Hashtag Bundler for X/Twitter and Bluesky

Hashtag Bundler v2.6 — Combined bundles with auto names, checkbox-based editor, collapsible UI, export/import, and improved UX.

当前为 2025-11-15 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Hashtag Bundler for X/Twitter and Bluesky
// @namespace    https://codymkw.nekoweb.org/
// @version      2.6
// @description  Hashtag Bundler v2.6 — Combined bundles with auto names, checkbox-based editor, collapsible UI, export/import, and improved UX.
// @match        https://twitter.com/*
// @match        https://x.com/*
// @match        https://bsky.app/*
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    //-------------------------------------------------------
    // Setup Host-Matching
    //-------------------------------------------------------
    let PANEL_ID, STORAGE_KEY;
    const host = location.hostname;

    if (host === "twitter.com" || host === "x.com") {
        PANEL_ID = "twitter-hashtag-panel";
        STORAGE_KEY = "twitterHashtagBundles";
    } else if (host === "bsky.app") {
        PANEL_ID = "bluesky-hashtag-panel";
        STORAGE_KEY = "blueskyHashtagBundles";
    } else return;

    const COMBINED_KEY = STORAGE_KEY + "_combined";
    const COLLAPSED_KEY = STORAGE_KEY + "_collapsed";

    //-------------------------------------------------------
    // Storage Helpers
    //-------------------------------------------------------
    const loadData = () => JSON.parse(GM_getValue(STORAGE_KEY, "{}") || "{}");
    const saveData = (v) => GM_setValue(STORAGE_KEY, JSON.stringify(v));

    const loadCombined = () => JSON.parse(GM_getValue(COMBINED_KEY, "{}") || "{}");
    const saveCombined = (v) => GM_setValue(COMBINED_KEY, JSON.stringify(v));

    const loadCollapsed = () => JSON.parse(GM_getValue(COLLAPSED_KEY, "false"));
    const saveCollapsed = (v) => GM_setValue(COLLAPSED_KEY, JSON.stringify(Boolean(v)));

    //-------------------------------------------------------
    // Helper to Create Elements
    //-------------------------------------------------------
    const el = (tag, props = {}, kids = []) => {
        const e = document.createElement(tag);
        for (const k in props) {
            if (k === "style" && typeof props[k] === "object") Object.assign(e.style, props[k]);
            else if (k === "html") e.innerHTML = props[k];
            else e[k] = props[k];
        }
        kids.forEach(c => e.appendChild(c));
        return e;
    };

    //-------------------------------------------------------
    // Auto Combined Name — Option C1
    //-------------------------------------------------------
    const makeAutoName = (sources) => {
        const initials = sources
            .map(n => n.split(/[\W_]+/).filter(Boolean)
                .map(w => w[0].toUpperCase()).join(""))
            .join("_");

        const safe = sources.map(s =>
            s.replace(/[^\w\-]/g, "_").replace(/_+/g, "_")
        );

        const ts = Date.now();
        return {
            key: `Combined_${safe.join("_")}_${ts}`,
            label: `Combined_${initials}_${String(ts).slice(-5)}`
        };
    };

    //-------------------------------------------------------
    // Confirmation Button Helper — text-only (Option D)
    //-------------------------------------------------------
    const confirmButton = (btn, originalText, confirmText) => {
        btn.textContent = confirmText;
        setTimeout(() => btn.textContent = originalText, 3000);
    };

    //-------------------------------------------------------
    // Create the Hashtag Panel
    //-------------------------------------------------------
    const createPanel = () => {
        if (document.getElementById(PANEL_ID)) return;

        const panel = el("div", {
            id: PANEL_ID,
            style: `
                position:fixed;right:20px;bottom:20px;width:320px;
                background:#1f1f1f;color:white;border-radius:12px;
                padding:10px;font-family:sans-serif;
                box-shadow:0 4px 14px rgba(0,0,0,0.5);
                z-index:999999;
            `
        });

        // Header ----------------------------------------------------------
        const header = el("div", {
            style: `
                display:flex;justify-content:space-between;align-items:center;
                cursor:pointer;margin-bottom:8px;
            `
        });

        const title = el("span", {
            textContent: "Hashtag Bundles",
            style: "font-weight:700;"
        });

        const arrow = el("span", {
            textContent: "▼",
            style: "font-size:14px;"
        });

        header.append(title, arrow);

        // Container --------------------------------------------------------
        const container = el("div", {
            id: "bundle-container",
            style: "max-height:380px;overflow-y:auto;padding-right:6px;"
        });

        panel.append(header, container);
        document.body.append(panel);

        // Handle collapse persistence
        let collapsed = loadCollapsed();
        container.style.display = collapsed ? "none" : "block";
        arrow.textContent = collapsed ? "▲" : "▼";

        header.onclick = () => {
            collapsed = !collapsed;
            saveCollapsed(collapsed);
            container.style.display = collapsed ? "none" : "block";
            arrow.textContent = collapsed ? "▲" : "▼";
        };

        renderPanel();

        //-------------------------------------------------------
        // Render Bundles inside Panel
        //-------------------------------------------------------
        function renderPanel() {
            container.innerHTML = "";

            const data = loadData();

            // NEW BUNDLE BUTTON --------------------------------------------------
            const newBtn = el("button", {
                textContent: "➕ New Bundle",
                style: `
                    width:100%;padding:8px;margin-bottom:8px;border:none;
                    border-radius:6px;background:#5865F2;color:white;
                    font-weight:700;cursor:pointer;
                `
            });
            newBtn.onclick = () => {
                const name = prompt("Bundle name?");
                if (!name) return;

                const tags = prompt("Enter hashtags (space separated):");
                if (!tags) return;

                data[name] = tags.split(/\s+/)
                    .filter(Boolean)
                    .map(t => t.startsWith("#") ? t : "#" + t);

                saveData(data);
                renderPanel();
            };
            container.append(newBtn);

            // COMBINE ------------------------------------------------------------
            const combineBtn = el("button", {
                textContent: "🔗 Combine Bundles",
                style: `
                    width:100%;padding:8px;margin-bottom:8px;border:none;
                    border-radius:6px;background:#9b59b6;color:white;
                    font-weight:700;cursor:pointer;
                `
            });
            combineBtn.onclick = openCreateCombinedModal;
            container.append(combineBtn);

            // SHOW COMBINED ------------------------------------------------------
            const showCombined = el("button", {
                textContent: "📂 Show Combined Bundles",
                style: `
                    width:100%;padding:8px;margin-bottom:8px;border:none;
                    border-radius:6px;background:#8e44ad;color:white;
                    font-weight:700;cursor:pointer;
                `
            });
            showCombined.onclick = openCombinedPopup;
            container.append(showCombined);

            // EXPORT / IMPORT ----------------------------------------------------
            const wrap = el("div", {
                style: "display:flex;gap:8px;margin:10px 0;"
            });

            const exportBtn = el("button", {
                textContent: "Export",
                style: `
                    flex:1;padding:8px;border:none;border-radius:6px;
                    background:#4CAF50;color:white;font-weight:700;cursor:pointer;
                `
            });
            exportBtn.onclick = () => {
                const blob = new Blob([JSON.stringify({
                    bundles: loadData(),
                    combined: loadCombined()
                }, null, 2)], { type: "application/json" });

                const a = document.createElement("a");
                a.href = URL.createObjectURL(blob);
                a.download = STORAGE_KEY + ".json";
                a.click();
            };

            const importBtn = el("button", {
                textContent: "Import",
                style: `
                    flex:1;padding:8px;border:none;border-radius:6px;
                    background:#2196F3;color:white;font-weight:700;cursor:pointer;
                `
            });
            importBtn.onclick = () => {
                const f = document.createElement("input");
                f.type = "file";
                f.accept = "application/json";
                f.onchange = e => {
                    const file = e.target.files[0];
                    if (!file) return;

                    const r = new FileReader();
                    r.onload = ev => {
                        try {
                            const j = JSON.parse(ev.target.result);
                            saveData({ ...loadData(), ...(j.bundles || {}) });
                            saveCombined({ ...loadCombined(), ...(j.combined || {}) });
                            renderPanel();
                            alert("Import complete.");
                        } catch {
                            alert("Invalid JSON.");
                        }
                    };
                    r.readAsText(file);
                };
                f.click();
            };

            wrap.append(exportBtn, importBtn);
            container.append(wrap);

            // LIST BUNDLES -------------------------------------------------------
            const names = Object.keys(data).sort();
            names.forEach(name => {
                const wrap = el("div", {
                    style: `
                        background:#262626;border-radius:8px;padding:8px;
                        margin-bottom:8px;
                    `
                });

                const row = el("div", {
                    style: `
                        display:flex;justify-content:space-between;align-items:center;
                        cursor:pointer;
                    `
                });

                const title = el("span", {
                    textContent: name,
                    style: "font-weight:700;font-size:14px;"
                });

                const arrow = el("span", {
                    textContent: "▶",
                    style: "font-size:13px;transition:0.18s;"
                });

                row.append(title, arrow);
                wrap.append(row);

                const inner = el("div", {
                    style: `
                        display:none;margin-top:8px;border-top:1px solid #444;
                        padding-top:8px;font-size:12px;color:#ccc;
                    `
                });

                const tags = el("div", {
                    textContent: data[name].join(" "),
                    style: "word-break:break-word;margin-bottom:8px;"
                });

                const btns = el("div", { style: "display:flex;gap:8px;" });

                const mkbtn = (t, bg, fn) =>
                    el("button", {
                        textContent: t,
                        style: `
                            flex:1;padding:6px;border:none;border-radius:6px;
                            background:${bg};color:white;font-weight:700;cursor:pointer;
                            font-size:12px;
                        `,
                        onclick: fn
                    });

                // Insert
                const insertBtn = mkbtn("Insert", "#2196F3", () => {
                    const c = document.querySelector("textarea, div[contenteditable='true']");
                    if (!c) return alert("Composer not found.");
                    const s = data[name].join(" ") + " ";
                    if (c.tagName === "TEXTAREA") c.value += s;
                    else document.execCommand("insertText", false, s);

                    confirmButton(insertBtn, "Insert", "Inserted!");
                });

                // Copy
                const copyBtn = mkbtn("Copy", "#4CAF50", async () => {
                    await navigator.clipboard.writeText(data[name].join(" "));
                    confirmButton(copyBtn, "Copy", "Copied!");
                });

                // Edit
                const editBtn = mkbtn("Edit", "#555", () => {
                    const tags2 = prompt("Edit hashtags:", data[name].join(" "));
                    if (!tags2) return;

                    data[name] = tags2.split(/\s+/)
                        .filter(Boolean)
                        .map(t => t.startsWith("#") ? t : "#" + t);

                    saveData(data);
                    renderPanel();
                });

                // Delete
                const delBtn = mkbtn("Delete", "#c0392b", () => {
                    if (!confirm(`Delete "${name}"?`)) return;
                    delete data[name];
                    saveData(data);
                    renderPanel();
                });

                btns.append(insertBtn, copyBtn, editBtn, delBtn);
                inner.append(tags, btns);
                wrap.append(inner);

                row.onclick = () => {
                    const open = inner.style.display === "none";
                    inner.style.display = open ? "block" : "none";
                    arrow.style.transform = open ? "rotate(90deg)" : "";
                };

                container.append(wrap);
            });
        }
    };

    //-------------------------------------------------------
    // Create Combined Bundle (checkbox modal)
    //-------------------------------------------------------
    const openCreateCombinedModal = () => {
        const data = loadData();
        const names = Object.keys(data).sort();
        if (!names.length) return alert("No bundles available.");

        if (document.getElementById("combine-backdrop")) return;

        const backdrop = el("div", {
            id: "combine-backdrop",
            style: `
                position:fixed;left:0;top:0;width:100%;height:100%;
                background:rgba(0,0,0,0.4);z-index:999999;
            `
        });
        document.body.append(backdrop);

        const modal = el("div", {
            style: `
                position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);
                background:#1f1f1f;color:white;width:360px;padding:14px;
                border-radius:12px;z-index:1000000;
                box-shadow:0 6px 24px rgba(0,0,0,0.6);
            `
        });
        backdrop.append(modal);

        backdrop.onclick = (e) => { if (e.target === backdrop) close(); };
        modal.onclick = (e) => e.stopPropagation();

        modal.append(el("div", {
            textContent: "Combine Bundles",
            style: "font-weight:700;margin-bottom:10px;font-size:15px;"
        }));

        let order = [];

        const list = el("div", {
            style: `
                display:flex;flex-direction:column;gap:6px;
                margin-bottom:10px;max-height:220px;overflow:auto;
            `
        });

        names.forEach(n => {
            const row = el("div", {
                style: `
                    display:flex;align-items:center;gap:8px;background:#262626;
                    padding:8px;border-radius:6px;cursor:pointer;
                `
            });
            const cb = el("input", { type: "checkbox" });
            cb.onclick = (e) => e.stopPropagation();
            cb.onchange = () => {
                if (cb.checked) order.push(n);
                else order = order.filter(x => x !== n);
                updatePreview();
            };
            const lbl = el("span", { textContent: n, style: "font-weight:600;" });
            lbl.onclick = () => { cb.checked = !cb.checked; cb.onchange(); };

            row.append(cb, lbl);
            list.append(row);
        });

        modal.append(list);

        modal.append(el("div", {
            textContent: "Preview:",
            style: "color:#ccc;margin-bottom:6px;"
        }));

        const preview = el("div", {
            style: `
                padding:8px;background:#262626;border-radius:6px;color:#ddd;
                min-height:28px;margin-bottom:12px;word-break:break-word;
            `
        });
        modal.append(preview);

        function updatePreview() {
            const tags = [...new Set(order.flatMap(n => data[n]))];
            preview.textContent = tags.join(" ") || "(none)";
        }

        const createBtn = el("button", {
            textContent: "Create Combined Bundle",
            style: `
                width:100%;padding:8px;background:#27ae60;color:white;
                border:none;border-radius:6px;font-weight:700;
                cursor:pointer;margin-bottom:8px;
            `
        });
        createBtn.onclick = () => {
            if (!order.length) return alert("Select at least one bundle.");

            const auto = makeAutoName(order);
            const tags = [...new Set(order.flatMap(n => data[n]))];

            const combined = loadCombined();
            combined[auto.key] = {
                label: auto.label,
                fullKey: auto.key,
                sources: [...order],
                tags
            };
            saveCombined(combined);

            alert("Combined bundle created.");
            close();
        };

        const cancelBtn = el("button", {
            textContent: "Cancel",
            style: `
                width:100%;padding:8px;background:#7f8c8d;color:white;
                border:none;border-radius:6px;font-weight:700;
                cursor:pointer;
            `
        });
        cancelBtn.onclick = close;

        modal.append(createBtn, cancelBtn);

        function close() { backdrop.remove(); }
    };

    //-------------------------------------------------------
    // Edit Combined Bundle (checkbox modal)
    //-------------------------------------------------------
    const openEditCombinedModal = (key) => {
        const combined = loadCombined();
        const data = loadData();

        if (!combined[key]) return alert("Not found.");
        const combo = combined[key];

        const names = Object.keys(data).sort();
        if (!names.length) return alert("No bundles exist.");

        if (document.getElementById("edit-backdrop")) return;

        const backdrop = el("div", {
            id: "edit-backdrop",
            style: `
                position:fixed;left:0;top:0;width:100%;height:100%;
                background:rgba(0,0,0,0.4);z-index:999999;
            `
        });
        document.body.append(backdrop);

        const modal = el("div", {
            style: `
                position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);
                background:#1f1f1f;color:white;width:360px;padding:14px;
                border-radius:12px;z-index:1000000;box-shadow:0 6px 24px rgba(0,0,0,0.6);
            `
        });
        backdrop.append(modal);

        backdrop.onclick = (e) => { if (e.target === backdrop) close(); };
        modal.onclick = (e) => e.stopPropagation();

        modal.append(el("div", {
            textContent: "Edit Combined Bundle",
            style: "font-weight:700;margin-bottom:10px;font-size:15px;"
        }));

        modal.append(el("div", {
            textContent: `Current label: ${combo.label}`,
            style: "color:#bbb;margin-bottom:10px;font-size:13px;"
        }));

        let order = [...combo.sources];

        const list = el("div", {
            style: `
                display:flex;flex-direction:column;gap:6px;margin-bottom:10px;
                max-height:220px;overflow:auto;
            `
        });

        names.forEach(n => {
            const row = el("div", {
                style: `
                    display:flex;align-items:center;gap:8px;background:#262626;
                    padding:8px;border-radius:6px;cursor:pointer;
                `
            });

            const cb = el("input", { type: "checkbox" });
            cb.checked = order.includes(n);
            cb.onclick = (e) => e.stopPropagation();
            cb.onchange = () => {
                if (cb.checked) {
                    if (!order.includes(n)) order.push(n);
                } else {
                    order = order.filter(x => x !== n);
                }
                updatePreview();
            };

            const lbl = el("span", { textContent: n, style: "font-weight:600;" });
            lbl.onclick = () => { cb.checked = !cb.checked; cb.onchange(); };

            row.append(cb, lbl);
            list.append(row);
        });

        modal.append(list);

        modal.append(el("div", {
            textContent: "Preview:",
            style: "color:#ccc;margin-bottom:6px;"
        }));

        const preview = el("div", {
            style: `
                padding:8px;background:#262626;border-radius:6px;color:#ddd;
                min-height:28px;margin-bottom:12px;word-break:break-word;
            `
        });
        modal.append(preview);

        function updatePreview() {
            const tags = [...new Set(order.flatMap(n => data[n] || []))];
            preview.textContent = tags.join(" ") || "(none)";
        }
        updatePreview();

        const saveBtn = el("button", {
            textContent: "Save Changes",
            style: `
                width:100%;padding:8px;background:#27ae60;color:white;
                border:none;border-radius:6px;font-weight:700;
                cursor:pointer;margin-bottom:8px;
            `
        });
        saveBtn.onclick = () => {
            if (!order.length) return alert("Select at least one bundle.");

            const oldKey = key;
            const oldSources = combo.sources.join("|");
            const newSources = order.join("|");

            let newKey = key;
            let newLabel = combo.label;

            if (oldSources !== newSources) {
                const auto = makeAutoName(order);
                newKey = auto.key;
                newLabel = auto.label;
                delete combined[oldKey];
            }

            const newTags = [...new Set(order.flatMap(n => data[n] || []))];

            combined[newKey] = {
                label: newLabel,
                fullKey: newKey,
                sources: [...order],
                tags: newTags
            };

            saveCombined(combined);
            alert("Updated.");
            close();
        };

        const cancelBtn = el("button", {
            textContent: "Cancel",
            style: `
                width:100%;padding:8px;background:#7f8c8d;color:white;
                border:none;border-radius:6px;font-weight:700;cursor:pointer;
            `
        });
        cancelBtn.onclick = close;

        modal.append(saveBtn, cancelBtn);

        function close() { backdrop.remove(); }
    };

    //-------------------------------------------------------
    // Combined Bundles Popup (with ⓧ close button)
    //-------------------------------------------------------
    const openCombinedPopup = () => {
        const combined = loadCombined();
        const keys = Object.keys(combined).sort((a, b) => {
            const la = (combined[a].label || a).toLowerCase();
            const lb = (combined[b].label || b).toLowerCase();
            return la.localeCompare(lb);
        });

        if (!keys.length) return alert("No combined bundles exist.");

        if (document.getElementById("combined-backdrop")) return;

        const backdrop = el("div", {
            id: "combined-backdrop",
            style: `
                position:fixed;left:0;top:0;width:100%;height:100%;
                background:rgba(0,0,0,0.4);z-index:999999;
            `
        });
        document.body.append(backdrop);

        const popup = el("div", {
            id: "combined-popup",
            style: `
                position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);
                background:#1f1f1f;color:white;width:420px;padding:18px;
                border-radius:12px;z-index:1000000;max-height:560px;overflow-y:auto;
                box-shadow:0 6px 24px rgba(0,0,0,0.6);
            `
        });
        backdrop.append(popup);

        backdrop.onclick = (e) => { if (e.target === backdrop) close(); };
        popup.onclick = (e) => e.stopPropagation();

        // Title
        popup.append(el("div", {
            textContent: "Combined Bundles",
            style: "font-weight:700;margin-bottom:12px;font-size:15px;"
        }));

        // CIRCLE CLOSE BUTTON (X4)
        const closeBtn = el("div", {
            html: "❌", // red circle X symbol
            style: `
                position:absolute;right:12px;top:12px;font-size:22px;
                cursor:pointer;color:#fff;opacity:0.85;
                user-select:none;
            `
        });
        closeBtn.onclick = () => close();
        closeBtn.onmouseenter = () => closeBtn.style.opacity = "1";
        closeBtn.onmouseleave = () => closeBtn.style.opacity = "0.85";
        popup.append(closeBtn);

        // Content
        keys.forEach(key => {
            const combo = combined[key];

            const wrap = el("div", {
                style: `
                    background:#262626;border-radius:8px;
                    padding:10px;margin-bottom:10px;
                `
            });

            const name = el("div", {
                textContent: combo.label,
                style: "font-weight:700;font-size:14px;margin-bottom:4px;"
            });

            const src = el("div", {
                textContent: "from: " + combo.sources.join(", "),
                style: "color:#bbb;font-size:12px;margin-bottom:8px;"
            });

            const tags = el("div", {
                textContent: combo.tags.join(" "),
                style: "color:#ddd;margin-bottom:8px;word-break:break-word;"
            });

            const btns = el("div", { style: "display:flex;gap:8px;" });

            const mkbtn = (t, bg, fn) =>
                el("button", {
                    textContent: t,
                    onclick: fn,
                    style: `
                        flex:1;padding:8px;border:none;border-radius:6px;
                        background:${bg};color:white;font-weight:700;
                        cursor:pointer;
                    `
                });

            // Insert
            const insertBtn = mkbtn("Insert", "#2196F3", () => {
                const c = document.querySelector("textarea, div[contenteditable='true']");
                if (!c) return alert("Composer not found.");
                const s = combo.tags.join(" ") + " ";
                if (c.tagName === "TEXTAREA") c.value += s;
                else document.execCommand("insertText", false, s);
                confirmButton(insertBtn, "Insert", "Inserted!");
            });

            // Copy
            const copyBtn = mkbtn("Copy", "#4CAF50", async () => {
                await navigator.clipboard.writeText(combo.tags.join(" "));
                confirmButton(copyBtn, "Copy", "Copied!");
            });

            const editBtn = mkbtn("Edit", "#f39c12", () => openEditCombinedModal(key));
            const delBtn = mkbtn("Delete", "#c0392b", () => {
                if (!confirm("Delete this combined bundle?")) return;
                const all = loadCombined();
                delete all[key];
                saveCombined(all);
                wrap.remove();
            });

            btns.append(insertBtn, copyBtn, editBtn, delBtn);
            wrap.append(name, src, tags, btns);
            popup.append(wrap);
        });

        function close() { backdrop.remove(); }
    };

    //-------------------------------------------------------
    // Observe for composer — auto show panel
    //-------------------------------------------------------
    let panelVisible = false;

    const observer = new MutationObserver(() => {
        const c = document.querySelector("textarea, div[contenteditable='true']");
        if (c && !panelVisible) {
            createPanel();
            panelVisible = true;
        } else if (!c && panelVisible) {
            const p = document.getElementById(PANEL_ID);
            if (p) p.remove();
            panelVisible = false;
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    if (document.querySelector("textarea, div[contenteditable='true']")) {
        createPanel();
        panelVisible = true;
    }

})();