Settings Tab Manager (STM)

Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator.

目前為 2025-04-23 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/533630/1575927/Settings%20Tab%20Manager%20%28STM%29.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Settings Tab Manager (STM)
// @namespace    shared-settings-manager
// @version      1.1.2
// @description  Provides an API for other userscripts to add tabs to a site's settings menu, with a single separator.
// @author       Gemini & User Input
// @license      MIT
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const MANAGER_ID = 'SettingsTabManager';
    const log = (...args) => console.log(`[${MANAGER_ID}]`, ...args);
    const warn = (...args) => console.warn(`[${MANAGER_ID}]`, ...args);
    const error = (...args) => console.error(`[${MANAGER_ID}]`, ...args);

    // --- Configuration ---
    const SELECTORS = Object.freeze({
        SETTINGS_MENU: '#settingsMenu',
        TAB_CONTAINER: '#settingsMenu .floatingContainer > div:first-child',
        PANEL_CONTAINER: '#settingsMenu .menuContentPanel',
        SITE_TAB: '.settingsTab',
        SITE_PANEL: '.panelContents',
        SITE_SEPARATOR: '.settingsTabSeparator', // Keep this if the site uses it visually
    });
    const ACTIVE_CLASSES = Object.freeze({
        TAB: 'selectedTab',
        PANEL: 'selectedPanel',
    });
    const ATTRS = Object.freeze({
        SCRIPT_ID: 'data-stm-script-id',
        MANAGED: 'data-stm-managed',
        SEPARATOR: 'data-stm-main-separator', // Attribute for the single separator
        ORDER: 'data-stm-order', // Attribute to store the order on the tab element
    });

    // --- State ---
    let isInitialized = false;
    let settingsMenuEl = null;
    let tabContainerEl = null;
    let panelContainerEl = null;
    let activeTabId = null;
    const registeredTabs = new Map(); // Stores { scriptId: config } for registered tabs
    const pendingRegistrations = []; // Stores { config } for tabs registered before init
    let isSeparatorAdded = false; // Flag to ensure only one separator is added

    // --- Readiness Promise ---
    let resolveReadyPromise;
    const readyPromise = new Promise(resolve => {
        resolveReadyPromise = resolve;
    });

    // --- Public API Definition ---
    const publicApi = Object.freeze({
        ready: readyPromise,
        registerTab: (config) => {
            return registerTabImpl(config);
        },
        activateTab: (scriptId) => {
            activateTabImpl(scriptId);
        },
        getPanelElement: (scriptId) => {
            return getPanelElementImpl(scriptId);
        },
        getTabElement: (scriptId) => {
            return getTabElementImpl(scriptId);
        }
    });

    // --- Styling ---
    GM_addStyle(`
        /* Ensure panels added by STM behave like native ones */
        ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}] {
            display: none; /* Hide inactive panels */
        }
        ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}].${ACTIVE_CLASSES.PANEL} {
            display: block; /* Show active panel */
        }
        /* Optional: Basic styling for the added tabs */
        ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] {
           cursor: pointer;
        }
        /* Styling for the single separator */
        ${SELECTORS.TAB_CONTAINER} > span[${ATTRS.SEPARATOR}] {
            cursor: default;
            margin: 0 5px; /* Add some spacing around the separator */
        }
    `);

    // --- Core Logic Implementation Functions ---

    /** Finds the essential DOM elements for the settings UI. Returns true if all found. */
    function findSettingsElements() {
        settingsMenuEl = document.querySelector(SELECTORS.SETTINGS_MENU);
        if (!settingsMenuEl) return false;

        tabContainerEl = settingsMenuEl.querySelector(SELECTORS.TAB_CONTAINER);
        panelContainerEl = settingsMenuEl.querySelector(SELECTORS.PANEL_CONTAINER);

        if (!tabContainerEl) {
            warn('Tab container not found within settings menu using selector:', SELECTORS.TAB_CONTAINER);
            return false;
        }
        if (!panelContainerEl) {
            warn('Panel container not found within settings menu using selector:', SELECTORS.PANEL_CONTAINER);
            return false;
        }
        // Ensure the elements are still in the document (relevant for re-init checks)
        if (!document.body.contains(settingsMenuEl) || !document.body.contains(tabContainerEl) || !document.body.contains(panelContainerEl)) {
             warn('Found settings elements are detached from the DOM.');
             settingsMenuEl = null;
             tabContainerEl = null;
             panelContainerEl = null;
             isSeparatorAdded = false; // Reset separator if containers are gone
             return false;
        }
        return true;
    }

    /** Deactivates the currently active STM tab (if any) and ensures native tabs are also visually deactivated. */
    function deactivateCurrentTab() {
        // Deactivate STM-managed tab
        if (activeTabId && registeredTabs.has(activeTabId)) {
            const config = registeredTabs.get(activeTabId);
            const tab = getTabElementImpl(activeTabId); // Use API getter
            const panel = getPanelElementImpl(activeTabId); // Use API getter

            tab?.classList.remove(ACTIVE_CLASSES.TAB);
            panel?.classList.remove(ACTIVE_CLASSES.PANEL);
            if (panel) panel.style.display = 'none'; // Explicitly hide panel via style

            try {
                config.onDeactivate?.(panel, tab);
            } catch (e) {
                error(`Error during onDeactivate for ${activeTabId}:`, e);
            }
        }
        activeTabId = null; // Clear active STM tab ID

        // Remove active class from any site tabs/panels managed outside STM
        panelContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_PANEL}.${ACTIVE_CLASSES.PANEL}:not([${ATTRS.MANAGED}])`)
            .forEach(p => p.classList.remove(ACTIVE_CLASSES.PANEL));
        tabContainerEl?.querySelectorAll(`:scope > ${SELECTORS.SITE_TAB}.${ACTIVE_CLASSES.TAB}:not([${ATTRS.MANAGED}])`)
            .forEach(t => t.classList.remove(ACTIVE_CLASSES.TAB));
    }

    /** Internal implementation for activating a specific STM tab */
    function activateTab(scriptId) {
        if (!registeredTabs.has(scriptId) || !tabContainerEl || !panelContainerEl) {
            warn(`Cannot activate tab: ${scriptId}. Not registered or containers not found.`);
            return;
        }

        if (activeTabId === scriptId) return; // Already active, do nothing

        deactivateCurrentTab(); // Deactivate previous one first (handles both STM and native)

        const config = registeredTabs.get(scriptId);
        const tab = getTabElementImpl(scriptId);
        const panel = getPanelElementImpl(scriptId);

        if (!tab || !panel) {
            error(`Tab or Panel element not found for ${scriptId} during activation.`);
            return;
        }

        tab.classList.add(ACTIVE_CLASSES.TAB);
        panel.classList.add(ACTIVE_CLASSES.PANEL);
        panel.style.display = 'block'; // Ensure panel is visible via style

        activeTabId = scriptId; // Set the new active STM tab ID

        try {
            config.onActivate?.(panel, tab);
        } catch (e) {
            error(`Error during onActivate for ${scriptId}:`, e);
        }
    }

    /** Handles clicks within the tab container to switch tabs. */
    function handleTabClick(event) {
        // Check if an STM-managed tab was clicked
        const clickedStmTab = event.target.closest(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`);
        if (clickedStmTab) {
            event.stopPropagation(); // Prevent potential conflicts with site listeners
            const scriptId = clickedStmTab.getAttribute(ATTRS.SCRIPT_ID);
            if (scriptId && scriptId !== activeTabId) {
                activateTab(scriptId); // Activate the clicked STM tab
            }
            return; // Handled
        }

        // Check if a native site tab was clicked (that isn't also an STM tab)
        const clickedSiteTab = event.target.closest(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`);
        if (clickedSiteTab) {
             // If a native tab is clicked, ensure any active STM tab is deactivated
            if (activeTabId) {
                 deactivateCurrentTab();
                 // Note: We rely on the site's own JS to handle activating the native tab/panel visually
            }
            return; // Let the site handle its own tab activation
        }

        // Check if the separator was clicked (do nothing)
        if (event.target.closest(`span[${ATTRS.SEPARATOR}]`)) {
             event.stopPropagation();
             return;
        }
    }


    /** Attaches the main click listener to the tab container. */
    function attachTabClickListener() {
        if (!tabContainerEl) return;
        // Use capture phase to potentially handle clicks before site's listeners if needed
        tabContainerEl.removeEventListener('click', handleTabClick, true);
        tabContainerEl.addEventListener('click', handleTabClick, true);
        log('Tab click listener attached.');
    }

     /** Helper to create the SINGLE separator span */
    function createSeparator() {
         const separator = document.createElement('span');
         // Use the site's separator class if defined, otherwise a fallback
         separator.className = SELECTORS.SITE_SEPARATOR ? SELECTORS.SITE_SEPARATOR.substring(1) : 'settings-tab-separator-fallback';
         separator.setAttribute(ATTRS.MANAGED, 'true'); // Mark as managed by STM (helps selection)
         separator.setAttribute(ATTRS.SEPARATOR, 'true'); // Mark as the main separator
         separator.textContent = '|'; // Or any desired visual separator
         // Style is handled by GM_addStyle now
         return separator;
     }

    /** Creates and inserts the tab and panel elements for a given script config. */
    function createTabAndPanel(config) {
        if (!tabContainerEl || !panelContainerEl) {
            error(`Cannot create tab/panel for ${config.scriptId}: Containers not found.`);
            return;
        }
        // Check if tab *element* exists already
        if (tabContainerEl.querySelector(`span[${ATTRS.SCRIPT_ID}="${config.scriptId}"]`)) {
            log(`Tab element already exists for ${config.scriptId}, skipping creation.`);
            return; // Avoid duplicates if registration happens multiple times somehow
        }

        log(`Creating tab/panel for: ${config.scriptId}`);

        // --- Create Tab ---
        const newTab = document.createElement('span');
        newTab.className = SELECTORS.SITE_TAB.substring(1); // Use site's base tab class
        newTab.textContent = config.tabTitle;
        newTab.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
        newTab.setAttribute(ATTRS.MANAGED, 'true');
        newTab.setAttribute('title', `${config.tabTitle} (Settings by ${config.scriptId})`);
        const desiredOrder = typeof config.order === 'number' ? config.order : Infinity;
        newTab.setAttribute(ATTRS.ORDER, desiredOrder); // Store order attribute

        // --- Create Panel ---
        const newPanel = document.createElement('div');
        newPanel.className = SELECTORS.SITE_PANEL.substring(1); // Use site's base panel class
        newPanel.setAttribute(ATTRS.SCRIPT_ID, config.scriptId);
        newPanel.setAttribute(ATTRS.MANAGED, 'true');
        newPanel.id = `${MANAGER_ID}-${config.scriptId}-panel`; // Unique ID for the panel

        // --- Insertion Logic (Single Separator & Ordered Tabs) ---
        let insertBeforeTab = null; // The specific STM tab element to insert *before*
        const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`));

        // Sort existing STM tabs DOM elements by their order attribute to find the correct insertion point
        existingStmTabs.sort((a, b) => {
            const orderA = parseInt(a.getAttribute(ATTRS.ORDER) || Infinity, 10);
            const orderB = parseInt(b.getAttribute(ATTRS.ORDER) || Infinity, 10);
            return orderA - orderB;
        });

        // Find the first existing STM tab with a higher order number than the new tab
        for (const existingTab of existingStmTabs) {
            const existingOrder = parseInt(existingTab.getAttribute(ATTRS.ORDER) || Infinity, 10);
            if (desiredOrder < existingOrder) {
                insertBeforeTab = existingTab;
                break;
            }
        }

        // Check if this is the very first STM tab being added *to the DOM*
        const isFirstStmTabBeingAdded = existingStmTabs.length === 0;
        let separatorInstance = null;

        // Add the single separator *only* if it hasn't been added yet AND this is the first STM tab going into the DOM
        if (!isSeparatorAdded && isFirstStmTabBeingAdded) {
            separatorInstance = createSeparator(); // Create the single separator instance
            isSeparatorAdded = true; // Mark it as added so it doesn't happen again
            log('Adding the main STM separator.');
        }

        // Insert the separator (if created) and then the tab
        if (insertBeforeTab) {
            // Insert before a specific existing STM tab
            if (separatorInstance) {
                // This case should technically not happen if separator is only added for the *first* tab,
                // but we keep it for robustness. It means we are inserting the *first* tab before another.
                tabContainerEl.insertBefore(separatorInstance, insertBeforeTab);
            }
            tabContainerEl.insertBefore(newTab, insertBeforeTab);
        } else {
            // Append after all existing STM tabs (or as the very first STM elements)
            if (separatorInstance) {
                // Append the separator first because this is the first STM tab
                tabContainerEl.appendChild(separatorInstance);
            }
            // Append the new tab (either after the new separator or after the last existing STM tab)
            tabContainerEl.appendChild(newTab);
        }

        // Append Panel to the panel container
        panelContainerEl.appendChild(newPanel);

        // --- Initialize Panel Content ---
        // Use Promise.resolve to handle both sync and async onInit functions gracefully
        try {
            Promise.resolve(config.onInit(newPanel, newTab)).catch(e => {
                error(`Error during async onInit for ${config.scriptId}:`, e);
                newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
            });
        } catch (e) { // Catch synchronous errors from onInit
            error(`Error during sync onInit for ${config.scriptId}:`, e);
            newPanel.innerHTML = `<p style="color: red;">Error initializing settings panel for ${config.scriptId}. See console.</p>`;
        }
    }

    /** Sorts and processes all pending registrations once the manager is initialized. */
    function processPendingRegistrations() {
        if (!isInitialized) return; // Should not happen if called correctly, but safety check
        log(`Processing ${pendingRegistrations.length} pending registrations...`);

        // Sort pending registrations by 'order' BEFORE creating elements
        pendingRegistrations.sort((a, b) => {
             const orderA = typeof a.order === 'number' ? a.order : Infinity;
             const orderB = typeof b.order === 'number' ? b.order : Infinity;
             // If orders are equal, maintain original registration order (stable sort behavior desired)
             return orderA - orderB;
        });

        // Process the sorted queue
        while (pendingRegistrations.length > 0) {
            const config = pendingRegistrations.shift(); // Get the next config from the front
            if (!registeredTabs.has(config.scriptId)) {
                registeredTabs.set(config.scriptId, config); // Add to map first
                // createTabAndPanel will handle DOM insertion and separator logic
                createTabAndPanel(config);
            } else {
                // This case should be rare now due to checks in registerTabImpl, but good to keep
                warn(`Script ID ${config.scriptId} was already registered. Skipping pending registration.`);
            }
        }
        log('Finished processing pending registrations.');
    }

    // --- Initialization and Observation ---

    /** Main initialization routine. Finds elements, attaches listener, processes queue. */
    function initializeManager() {
        if (!findSettingsElements()) {
            // log('Settings elements not found or invalid on init check.');
            return false; // Keep observer active
        }

        // Check if already initialized *and* elements are still valid
        if (isInitialized && settingsMenuEl && tabContainerEl && panelContainerEl) {
            // log('Manager already initialized and elements seem valid.');
            attachTabClickListener(); // Re-attach listener just in case it got removed
            return true;
        }

        log('Initializing Settings Tab Manager...');
        attachTabClickListener(); // Attach the main click listener

        isInitialized = true; // Set flag *before* resolving promise and processing queue
        log('Manager is ready.');

        // Resolve the public promise *after* setting isInitialized flag
        // This signals to waiting scripts that the manager is ready
        resolveReadyPromise(publicApi);

        // Process any registrations that occurred before initialization was complete
        processPendingRegistrations();

        return true; // Initialization successful
    }

    // --- Mutation Observer ---
    // Observes the body for the appearance of the settings menu
    const observer = new MutationObserver((mutationsList, obs) => {
        let needsReInitCheck = false;

        // 1. Check if the menu exists but we aren't initialized yet
        if (!isInitialized && document.querySelector(SELECTORS.SETTINGS_MENU)) {
            needsReInitCheck = true;
        }
        // 2. Check if the menu *was* found previously but is no longer in the DOM
        else if (isInitialized && settingsMenuEl && !document.body.contains(settingsMenuEl)) {
            warn('Settings menu seems to have been removed from DOM.');
            isInitialized = false; // Force re-initialization if it reappears
            settingsMenuEl = null;
            tabContainerEl = null;
            panelContainerEl = null;
            isSeparatorAdded = false; // Reset separator flag
            activeTabId = null;
            // Don't resolve the promise again, but new scripts might await it
            // Reset the promise state if needed (more complex, usually not necessary)
            // readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; });
            needsReInitCheck = true; // Check if it got immediately re-added
        }

        // 3. Scan mutations if we haven't found the menu yet or if it was removed
        if (!settingsMenuEl || needsReInitCheck) {
             for (const mutation of mutationsList) {
                if (mutation.addedNodes) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // Check if the added node *is* the menu or *contains* the menu
                             const menu = (node.matches && node.matches(SELECTORS.SETTINGS_MENU))
                                            ? node
                                            : (node.querySelector ? node.querySelector(SELECTORS.SETTINGS_MENU) : null);
                            if (menu) {
                                log('Settings menu detected in DOM via MutationObserver.');
                                needsReInitCheck = true;
                                break; // Found it in this mutation
                            }
                        }
                    }
                }
                if (needsReInitCheck) break; // Found it, no need to check other mutations
            }
        }

        // 4. Attempt initialization if needed
        if (needsReInitCheck) {
            // Use setTimeout to allow the DOM to potentially stabilize after the mutation
            setTimeout(() => {
                if (initializeManager()) {
                    log('Manager initialized/re-initialized successfully via MutationObserver.');
                    // Optional: If the settings menu is known to be stable once added,
                    // you *could* disconnect the observer here to save resources.
                    // However, leaving it active is safer if the menu might be rebuilt.
                    // obs.disconnect();
                    // log('Mutation observer disconnected.');
                } else {
                    // log('Initialization check failed after mutation, observer remains active.');
                }
            }, 0); // Delay slightly
         }
    });

    // Start observing the body for additions/removals in the subtree
    observer.observe(document.body, {
        childList: true, // Watch for direct children changes (adding/removing nodes)
        subtree: true    // Watch the entire subtree under document.body
    });
    log('Mutation observer started for settings menu detection.');

    // --- Attempt initial initialization on script load ---
    // Use setTimeout to ensure this runs after the current execution context,
    // allowing the global API exposure to happen first.
    setTimeout(initializeManager, 0);


    // --- API Implementation Functions ---

    /** Public API function to register a new settings tab. */
    function registerTabImpl(config) {
        // --- Input Validation ---
        if (!config || typeof config !== 'object') {
            error('Registration failed: Invalid config object provided.'); return false;
        }
        const { scriptId, tabTitle, onInit } = config;
        if (typeof scriptId !== 'string' || !scriptId.trim()) {
             error('Registration failed: Invalid or missing scriptId (string).', config); return false;
        }
        if (typeof tabTitle !== 'string' || !tabTitle.trim()) {
             error('Registration failed: Invalid or missing tabTitle (string).', config); return false;
        }
        if (typeof onInit !== 'function') {
             error('Registration failed: onInit callback must be a function.', config); return false;
        }
        // Optional callbacks validation
        if (config.onActivate && typeof config.onActivate !== 'function') {
             error(`Registration for ${scriptId} failed: onActivate (if provided) must be a function.`); return false;
        }
        if (config.onDeactivate && typeof config.onDeactivate !== 'function') {
             error(`Registration for ${scriptId} failed: onDeactivate (if provided) must be a function.`); return false;
        }
        // Optional order validation
        if (config.order !== undefined && typeof config.order !== 'number') {
            warn(`Registration for ${scriptId}: Invalid order value provided (must be a number). Defaulting to end.`, config);
            // Allow registration but ignore invalid order
            delete config.order;
        }

        // Prevent duplicate registrations by scriptId
        if (registeredTabs.has(scriptId) || pendingRegistrations.some(p => p.scriptId === scriptId)) {
             warn(`Registration failed: Script ID "${scriptId}" is already registered or pending.`); return false;
        }

        // --- Registration Logic ---
        log(`Registration accepted for: ${scriptId}`);
        // Clone config to avoid external modification? (Shallow clone is usually sufficient)
        const registrationData = { ...config };

        if (isInitialized) {
            // Manager ready: Register immediately and create elements
            registeredTabs.set(scriptId, registrationData);
            // createTabAndPanel handles sorting/insertion/separator logic
            createTabAndPanel(registrationData);
        } else {
            // Manager not ready: Add to pending queue
            log(`Manager not ready, queueing registration for ${scriptId}`);
            pendingRegistrations.push(registrationData);
             // Sort pending queue immediately upon adding to maintain correct insertion order later
             pendingRegistrations.sort((a, b) => {
                 const orderA = typeof a.order === 'number' ? a.order : Infinity;
                 const orderB = typeof b.order === 'number' ? b.order : Infinity;
                 return orderA - orderB;
             });
        }
        return true; // Indicate successful acceptance of registration (not necessarily immediate creation)
    }

    /** Public API function to programmatically activate a registered tab. */
    function activateTabImpl(scriptId) {
        if (typeof scriptId !== 'string' || !scriptId.trim()) {
             error('activateTab failed: Invalid scriptId provided.');
             return;
        }
        if (isInitialized) {
            // Call the internal function which handles the logic
            activateTab(scriptId);
        } else {
            // Queue activation? Or just warn? Warning is simpler.
            warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`);
            // Could potentially store a desired initial tab and activate it in processPendingRegistrations
        }
    }

    /** Public API function to get the DOM element for a tab's panel. */
    function getPanelElementImpl(scriptId) {
        if (!isInitialized || !panelContainerEl) return null;
        if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
        // Use attribute selector for robustness
        return panelContainerEl.querySelector(`div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
    }

    /** Public API function to get the DOM element for a tab's clickable part. */
    function getTabElementImpl(scriptId) {
        if (!isInitialized || !tabContainerEl) return null;
        if (typeof scriptId !== 'string' || !scriptId.trim()) return null;
        // Use attribute selector for robustness
        return tabContainerEl.querySelector(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
    }


    // --- Global Exposure ---
    // Expose the public API on the window object, checking for conflicts.
    if (window.SettingsTabManager && window.SettingsTabManager !== publicApi) {
        // A different instance or script already exists. Log a warning.
        // Depending on requirements, could throw an error, merge APIs, or namespace.
        warn('window.SettingsTabManager is already defined by another script or instance! Potential conflict.');
    } else if (!window.SettingsTabManager) {
        // Define the API object on the window, making it non-writable but configurable.
        Object.defineProperty(window, 'SettingsTabManager', {
            value: publicApi,
            writable: false,      // Prevents accidental overwriting of the API object itself
            configurable: true    // Allows users to delete or redefine it if necessary (e.g., for debugging)
        });
        log('SettingsTabManager API exposed on window.');
    }

})(); // End of IIFE