8chan Settings Tab Manager (STM) [Core API]

Core API - Provides window.SettingsTabManager for other scripts to add settings tabs.

Ekde 2025/04/23. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         8chan Settings Tab Manager (STM) [Core API]
// @namespace    nipah-scripts-8chan
// @version      1.2.0
// @description  Core API - Provides window.SettingsTabManager for other scripts to add settings tabs.
// @author       nipah, Gemini
// @license      MIT
// @match        https://8chan.moe/*
// @match        https://8chan.se/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Constants ---
    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);

    const SELECTORS = Object.freeze({
        SETTINGS_MENU: '#settingsMenu',
        TAB_CONTAINER: '#settingsMenu .floatingContainer > div:first-child',
        PANEL_CONTAINER: '#settingsMenu .menuContentPanel',
        SITE_TAB: '.settingsTab',          // Generic class for any tab (native or STM)
        SITE_PANEL: '.panelContents',      // Generic class for any panel (native or STM)
        SITE_SEPARATOR: '.settingsTabSeparator', // Class for separators (native or STM)
    });
    const ACTIVE_CLASSES = Object.freeze({
        TAB: 'selectedTab',
        PANEL: 'selectedPanel',
    });
    const ATTRS = Object.freeze({
        SCRIPT_ID: 'data-stm-script-id', // Identifies the script managing the tab/panel
        MANAGED: 'data-stm-managed',     // Marks elements managed by STM
        SEPARATOR: 'data-stm-main-separator', // Marks the STM main separator
        ORDER: 'data-stm-order',         // Stores the desired display order for STM tabs
    });

    // --- State ---
    let isInitialized = false;
    let settingsMenuEl = null;
    let tabContainerEl = null;
    let panelContainerEl = null;
    /** @type {string | null} Tracks the scriptId of the *currently active* STM tab. Null if a native tab is active or no tab is active. */
    let activeStmTabId = null;
    /** @type {Map<string, object>} Stores registered tab configurations: { scriptId: config } */
    const registeredTabs = new Map();
    /** @type {Array<object>} Stores configurations for tabs registered before the manager is initialized. */
    const pendingRegistrations = [];
    let isSeparatorAdded = false; // Flag to ensure only one main separator is added.

    // --- Readiness Promise ---
    let resolveReadyPromise;
    /** @type {Promise<object>} A promise that resolves with the public API when the manager is ready. */
    const readyPromise = new Promise(resolve => { resolveReadyPromise = resolve; });

    // --- Public API Definition ---
    const publicApi = Object.freeze({
        /** A Promise that resolves with the API object itself once the manager is initialized and ready. */
        ready: readyPromise,
        /**
         * Registers a new settings tab.
         * @param {object} config - The configuration object for the tab.
         * @param {string} config.scriptId - A unique identifier for the script registering the tab.
         * @param {string} config.tabTitle - The text displayed on the tab.
         * @param {(panelElement: HTMLDivElement, tabElement: HTMLSpanElement) => void | Promise<void>} config.onInit - Callback function executed once when the tab and panel are first created. Receives the panel's content div and the tab span element. Can be async.
         * @param {number} [config.order] - Optional number to control the tab's position relative to other STM tabs (lower numbers appear first). Defaults to appearing last.
         * @param {(panelElement: HTMLDivElement | null, tabElement: HTMLSpanElement | null) => void} [config.onActivate] - Optional callback executed when this tab becomes active. Receives the panel and tab elements (or null if lookup fails).
         * @param {(panelElement: HTMLDivElement | null, tabElement: HTMLSpanElement | null) => void} [config.onDeactivate] - Optional callback executed when this tab becomes inactive. Receives the panel and tab elements (or null if lookup fails).
         * @returns {boolean} True if the registration was accepted (either processed immediately or queued), false if validation failed.
         */
        registerTab: (config) => registerTabImpl(config),
        /**
         * Programmatically activates a registered STM tab.
         * @param {string} scriptId - The unique scriptId of the tab to activate.
         */
        activateTab: (scriptId) => activateTabImpl(scriptId),
        /**
         * Retrieves the DOM element for a registered tab's content panel.
         * @param {string} scriptId - The scriptId of the tab.
         * @returns {HTMLDivElement | null} The panel element, or null if not found or not initialized.
         */
        getPanelElement: (scriptId) => getPanelElementImpl(scriptId),
        /**
         * Retrieves the DOM element for a registered tab's clickable tab element.
         * @param {string} scriptId - The scriptId of the tab.
         * @returns {HTMLSpanElement | null} The tab element, or null if not found or not initialized.
         */
        getTabElement: (scriptId) => getTabElementImpl(scriptId)
    });

    // --- Styling ---
    GM_addStyle(`
        /* Ensure panels added by STM behave like native ones */
        ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}] {
            display: none; /* Hide inactive panels */
        }
        ${SELECTORS.PANEL_CONTAINER} > div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}].${ACTIVE_CLASSES.PANEL} {
            display: block; /* Show active panel */
        }
        /* 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 */
            user-select: none; /* Prevent text selection */
            -webkit-user-select: none;
        }
    `);

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

    /**
     * Finds and caches the essential DOM elements for the settings UI.
     * @returns {boolean} True if all required elements are found and attached to the DOM, false otherwise.
     */
    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
             activeStmTabId = null; // Reset active tab state
             return false;
        }
        return true;
    }

    /**
     * Internal helper: Deactivates the specified STM tab visually and triggers its onDeactivate callback.
     * Does *not* change `activeStmTabId`.
     * @param {string} scriptId The ID of the STM tab to deactivate.
     */
    function _deactivateStmTabVisualsAndCallback(scriptId) {
        if (!scriptId) return;

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

        tab?.classList.remove(ACTIVE_CLASSES.TAB);
        if (panel) {
            panel.classList.remove(ACTIVE_CLASSES.PANEL);
            // Ensure display is none, overriding potential inline styles from activation
            panel.style.display = 'none';
        }

        // Call the script's deactivate hook only if config exists
        if (config?.onDeactivate) {
            try {
                config.onDeactivate(panel, tab); // Pass potentially null elements
            } catch (e) {
                error(`Error during onDeactivate for ${scriptId}:`, e);
            }
        }
    }

    /**
     * Internal helper: Activates the specified STM tab visually and triggers its onActivate callback.
     * Does *not* change `activeStmTabId` or deactivate other tabs.
     * @param {string} scriptId The ID of the STM tab to activate.
     * @returns {boolean} True if activation visuals/callback were attempted successfully (elements found), false otherwise.
     */
    function _activateStmTabVisualsAndCallback(scriptId) {
        const config = registeredTabs.get(scriptId);
        if (!config) {
            error(`Cannot activate visuals: Config not found for ${scriptId}.`);
            return false;
        }

        const tab = getTabElementImpl(scriptId);
        const panel = getPanelElementImpl(scriptId);

        if (!tab || !panel) {
            error(`Cannot activate visuals: Tab or Panel element not found for ${scriptId}.`);
            return false;
        }

        // Activate the new STM tab/panel visuals
        tab.classList.add(ACTIVE_CLASSES.TAB);
        panel.classList.add(ACTIVE_CLASSES.PANEL);
        // Explicitly set display to block, ensuring visibility even if default CSS is overridden elsewhere
        panel.style.display = 'block';

        // Call the script's activation hook
        if (config.onActivate) {
            try {
                config.onActivate(panel, tab);
            } catch (e) {
                error(`Error during onActivate for ${scriptId}:`, e);
                // Activation visuals already applied, difficult to revert cleanly. Error logged.
            }
        }
        return true; // Activation attempted
    }

    /**
     * Deactivates all *native* (non-STM) tabs and panels.
     * Used when switching from a native tab to an STM tab.
     */
    function _deactivateNativeTabs() {
        if (!tabContainerEl || !panelContainerEl) return;

        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));
        // log('Deactivated native tabs/panels.');
    }

    /**
     * Handles clicks within the tab container to switch between native and STM tabs.
     * Uses event delegation on the tab container.
     * @param {MouseEvent} event - The click event.
     */
    function handleTabClick(event) {
        // Ensure containers are still valid before proceeding
        if (!tabContainerEl || !panelContainerEl || !event.target) {
            return;
        }

        // Find the closest ancestor that is a tab element (native or STM)
        const clickedTabElement = event.target.closest(SELECTORS.SITE_TAB);
        if (!clickedTabElement) {
            // Clicked outside any tab (e.g., empty space, maybe separator if not matched by SITE_TAB)
            // Check specifically for our separator
            if (event.target.closest(`span[${ATTRS.SEPARATOR}]`)) {
                event.stopPropagation(); // Prevent any site action if separator is clicked
                return;
            }
            return; // Not a relevant click
        }

        const isStmTab = clickedTabElement.matches(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`);
        const clickedStmScriptId = isStmTab ? clickedTabElement.getAttribute(ATTRS.SCRIPT_ID) : null;

        // --- Case 1: Clicked an STM Tab ---
        if (isStmTab && clickedStmScriptId) {
            event.stopPropagation(); // Prevent site's default handler for tabs

            if (clickedStmScriptId === activeStmTabId) {
                // log(`Clicked already active STM tab: ${clickedStmScriptId}`);
                return; // Already active, do nothing
            }

            // Deactivate previously active tab (could be native or another STM tab)
            if (activeStmTabId) {
                _deactivateStmTabVisualsAndCallback(activeStmTabId); // Deactivate previous STM tab
            } else {
                _deactivateNativeTabs(); // Deactivate native tabs if one was active
            }

            // Activate the clicked STM tab
            if (_activateStmTabVisualsAndCallback(clickedStmScriptId)) {
                activeStmTabId = clickedStmScriptId; // Update state *after* successful activation attempt
            } else {
                // Activation failed (elements missing?) - log already happened inside helper.
                // Reset state to avoid inconsistent UI.
                activeStmTabId = null;
            }
            return; // Handled by STM
        }

        // --- Case 2: Clicked a Native Site Tab ---
        // Check it's specifically a non-managed tab to avoid accidental matches if selectors overlap badly
        if (clickedTabElement.matches(`${SELECTORS.SITE_TAB}:not([${ATTRS.MANAGED}])`)) {
            // If an STM tab *was* active, deactivate it visually and clear STM's active state.
            if (activeStmTabId) {
                // log(`Deactivating current STM tab (${activeStmTabId}) due to native tab click.`);
                _deactivateStmTabVisualsAndCallback(activeStmTabId);
                activeStmTabId = null; // Clear STM state, site handler will manage native state
            }

            // **Allow event propagation** - Let the site's own click handler manage the native tab switch.
            // log("Allowing event propagation for native tab handler.");
            return;
        }

        // --- Case 3: Clicked something else matching SITE_TAB (e.g., STM separator if it has SITE_TAB class) ---
        // If it's our separator, stop propagation. (This check might be redundant if separator doesn't have SITE_TAB class)
        if (clickedTabElement.matches(`span[${ATTRS.SEPARATOR}]`)) {
            event.stopPropagation();
            return;
        }

        // Otherwise, ignore (shouldn't happen often if selectors are correct).
    }

    /** Attaches the main click listener to the tab container using the capture phase. */
    function attachTabClickListener() {
        if (!tabContainerEl) return;
        // Remove existing listener before adding to prevent duplicates if re-initialized
        tabContainerEl.removeEventListener('click', handleTabClick, true);
        tabContainerEl.addEventListener('click', handleTabClick, true); // Use capture phase to run before site's potential handlers
        log('Tab click listener attached.');
    }

    /**
     * Creates the single separator span element used between native and STM tabs.
     * @returns {HTMLSpanElement} The separator element.
     */
    function createSeparator() {
         const separator = document.createElement('span');
         // Use the site's separator class if defined, otherwise a fallback
         separator.className = SELECTORS.SITE_SEPARATOR.startsWith('.')
            ? SELECTORS.SITE_SEPARATOR.substring(1)
            : 'settings-tab-separator-fallback'; // Basic fallback class name
         separator.setAttribute(ATTRS.MANAGED, 'true');
         separator.setAttribute(ATTRS.SEPARATOR, 'true');
         separator.textContent = '|'; // Simple visual separator
         return separator;
     }

    /**
     * Creates and inserts the tab and panel DOM elements for a given script configuration.
     * Handles ordering and the single separator insertion.
     * @param {object} config - The registration configuration object for the tab.
     */
    function createTabAndPanel(config) {
        if (!tabContainerEl || !panelContainerEl) {
            error(`Cannot create tab/panel for ${config.scriptId}: Containers not found.`);
            return;
        }
        // Avoid creating duplicates if called multiple times (e.g., during re-init)
        if (getTabElementImpl(config.scriptId) || getPanelElementImpl(config.scriptId)) {
            log(`Tab or panel element already exists for ${config.scriptId}, skipping creation.`);
            return;
        }

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

        // --- Create Tab Element ---
        const newTab = document.createElement('span');
        newTab.className = SELECTORS.SITE_TAB.substring(1); // Use site's 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 = config.order ?? Infinity; // Use nullish coalescing for default
        newTab.setAttribute(ATTRS.ORDER, desiredOrder);

        // --- Create Panel Element ---
        const newPanel = document.createElement('div');
        newPanel.className = SELECTORS.SITE_PANEL.substring(1); // Use site's 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 (Separator & Ordering) ---
        let insertBeforeTab = null;
        // Get existing STM tabs, convert NodeList to Array for sorting
        const existingStmTabs = Array.from(tabContainerEl.querySelectorAll(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}]`));

        // Sort existing tabs by their order attribute to find the correct insertion point
        existingStmTabs.sort((a, b) => (parseFloat(a.getAttribute(ATTRS.ORDER) ?? Infinity)) - (parseFloat(b.getAttribute(ATTRS.ORDER) ?? Infinity)));

        // Find the first existing STM tab with a higher order number
        for (const existingTab of existingStmTabs) {
            if (desiredOrder < (parseFloat(existingTab.getAttribute(ATTRS.ORDER) ?? Infinity))) {
                insertBeforeTab = existingTab;
                break;
            }
        }

        // Add the separator only once, before the *first* STM tab is added
        let separatorInstance = null;
        if (!isSeparatorAdded && existingStmTabs.length === 0) {
            separatorInstance = createSeparator();
            isSeparatorAdded = true; // Set flag
            log('Adding the main STM separator.');
        }

        // Perform the insertion
        if (insertBeforeTab) {
            // Insert separator (if created) and new tab before the found element
            if (separatorInstance) tabContainerEl.insertBefore(separatorInstance, insertBeforeTab);
            tabContainerEl.insertBefore(newTab, insertBeforeTab);
        } else {
            // Append separator (if created) and new tab to the end
            if (separatorInstance) tabContainerEl.appendChild(separatorInstance);
            tabContainerEl.appendChild(newTab);
        }
        // Append the panel to the panel container (order doesn't matter visually here)
        panelContainerEl.appendChild(newPanel);

        // --- Initialize Panel Content (Safely) ---
        try {
            // Use Promise.resolve to handle both sync and async onInit functions gracefully
            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>`;
        }
    }

    /** Processes all pending registrations that arrived before initialization was complete. */
    function processPendingRegistrations() {
        if (!isInitialized) return; // Should not happen if called correctly, but safe check
        if (pendingRegistrations.length === 0) return; // Nothing to process

        log(`Processing ${pendingRegistrations.length} pending registrations...`);

        // Sort the pending queue by order *before* processing
        pendingRegistrations.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));

        while (pendingRegistrations.length > 0) {
            const config = pendingRegistrations.shift(); // Process in sorted order
            // Double-check it wasn't somehow registered already (e.g., edge case with re-init)
            if (!registeredTabs.has(config.scriptId)) {
                registeredTabs.set(config.scriptId, config);
                createTabAndPanel(config); // This function handles ordering relative to existing tabs
            } else {
                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, resolves ready promise.
     * @returns {boolean} True if initialization was successful, false otherwise.
     */
    function initializeManager() {
        // Attempt to find the core elements
        if (!findSettingsElements()) {
            // log('Settings elements not found yet.');
            return false; // Cannot initialize
        }

        // If already initialized AND the elements are still valid, just ensure listener is attached and exit.
        // This handles cases where the observer might trigger initialize multiple times without DOM changes.
        if (isInitialized && settingsMenuEl && tabContainerEl && panelContainerEl) {
            // log('Manager already initialized and elements are valid. Ensuring listener is attached.');
            attachTabClickListener(); // Re-attach listener just in case it was somehow removed
            return true;
        }

        log('Initializing Settings Tab Manager...');
        attachTabClickListener();
        isInitialized = true;
        log('Manager is now initialized and ready.');

        // Process any registrations that occurred before init completed
        processPendingRegistrations();

        // Resolve the public promise to signal readiness to consumer scripts
        resolveReadyPromise(publicApi);

        return true;
    }

    /**
     * MutationObserver callback to detect when the settings menu is added to or removed from the DOM.
     * @param {MutationRecord[]} mutationsList - List of mutations that occurred.
     * @param {MutationObserver} obs - The observer instance.
     */
    function handleDOMChanges(mutationsList, obs) {
        let needsReInitCheck = false;
        let settingsMenuFoundInMutation = false;

        // Scenario 1: Manager isn't initialized yet, check if the settings menu was added.
        if (!isInitialized) {
            if (document.querySelector(SELECTORS.SETTINGS_MENU)) {
                 // Menu exists in the DOM now, maybe added before observer caught it or by this mutation.
                 needsReInitCheck = true;
                 settingsMenuFoundInMutation = true; // Assume it might have been added
            } else {
                 // Menu doesn't exist, check if it was added in this specific mutation batch
                 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 itself is the menu, or if it contains the menu
                                 const menu = (node.matches?.(SELECTORS.SETTINGS_MENU)) ? node : node.querySelector?.(SELECTORS.SETTINGS_MENU);
                                 if (menu) {
                                     log('Settings menu detected in addedNodes via MutationObserver.');
                                     needsReInitCheck = true;
                                     settingsMenuFoundInMutation = true;
                                     break; // Found it
                                 }
                             }
                         }
                     }
                     if (settingsMenuFoundInMutation) break; // Stop checking mutations if found
                 }
            }
        }
        // Scenario 2: Manager *was* initialized, check if the menu element was removed.
        else if (isInitialized && settingsMenuEl && !document.body.contains(settingsMenuEl)) {
            warn('Settings menu seems to have been removed from the DOM. De-initializing.');
            // Reset state
            isInitialized = false;
            settingsMenuEl = null;
            tabContainerEl = null;
            panelContainerEl = null;
            isSeparatorAdded = false;
            activeStmTabId = null;
            // Don't clear registeredTabs or pendingRegistrations, they might be needed if menu reappears
            // We need to check again later if the menu reappears
            needsReInitCheck = true; // Trigger a check in case it was immediately re-added
        }

        // If a check is needed (menu added or removed), attempt initialization asynchronously.
        // Using setTimeout defers execution slightly, allowing the DOM to stabilize after mutations.
        if (needsReInitCheck) {
            // log('Mutation detected, scheduling initialization/re-check.'); // Can be verbose
            setTimeout(() => {
                if (initializeManager()) {
                    // Successfully initialized or re-initialized
                    if (settingsMenuFoundInMutation) {
                        log('Manager initialized successfully following menu detection.');
                    } else {
                        // This case might occur if the menu was removed and re-added quickly,
                        // or if initializeManager was called due to state reset.
                        log('Manager state updated/re-initialized.');
                    }
                } else {
                    // Initialization failed (e.g., menu found but internal structure missing)
                    // log('Initialization attempt after mutation failed.'); // Can be verbose
                }
            }, 0);
        }
    }

    // --- Start Observer ---
    const observer = new MutationObserver(handleDOMChanges);
    observer.observe(document.body, {
        childList: true, // Watch for addition/removal of child nodes
        subtree: true    // Watch descendants as well
    });
    log('Mutation observer started for settings menu detection.');

    // --- Initial Initialization Attempt ---
    // Try to initialize immediately after script runs, in case the menu is already present.
    // Use setTimeout to ensure it runs after the current execution context.
    setTimeout(initializeManager, 0);


    // --- API Implementation Functions ---

    /** @inheritdoc */
    function registerTabImpl(config) {
        // --- Input Validation ---
        if (!config || typeof config !== 'object') { error('Registration failed: Invalid config object provided.'); return false; }
        const { scriptId, tabTitle, onInit } = config; // Destructure required fields for checks

        // Validate required fields
        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 for ${scriptId}: Invalid or missing tabTitle (string).`, config); return false; }
        if (typeof onInit !== 'function') { error(`Registration failed for ${scriptId}: onInit callback must be a function.`, config); return false; }

        // Validate optional fields
        if (config.onActivate !== undefined && typeof config.onActivate !== 'function') { error(`Registration for ${scriptId} failed: onActivate (if provided) must be a function.`); return false; }
        if (config.onDeactivate !== undefined && typeof config.onDeactivate !== 'function') { error(`Registration for ${scriptId} failed: onDeactivate (if provided) must be a function.`); return false; }
        if (config.order !== undefined && (typeof config.order !== 'number' || !isFinite(config.order))) {
            warn(`Registration for ${scriptId}: Invalid 'order' value provided (must be a finite number). Using default order.`, config);
            delete config.order; // Remove invalid order so default (Infinity) applies
        }

        // Check for duplicates
        if (registeredTabs.has(scriptId) || pendingRegistrations.some(p => p.scriptId === scriptId)) {
            warn(`Registration failed: Script ID "${scriptId}" is already registered or pending registration.`);
            return false;
        }
        // --- End Validation ---

        log(`Registration accepted for: ${scriptId}`);
        const registrationData = { ...config }; // Shallow clone to avoid external modification

        if (isInitialized) {
            // Manager is ready, register and create elements immediately
            registeredTabs.set(scriptId, registrationData);
            createTabAndPanel(registrationData); // Handles insertion order
        } else {
            // Manager not ready, add to the pending queue
            log(`Manager not ready, queueing registration for ${scriptId}`);
            pendingRegistrations.push(registrationData);
            // Keep queue sorted for predictable processing later (though createTabAndPanel handles final order)
            pendingRegistrations.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
        }
        return true; // Registration accepted (processed or queued)
    }

    /** @inheritdoc */
    function activateTabImpl(scriptId) {
        // console.trace(`[${MANAGER_ID}] activateTabImpl called with ID: ${scriptId}`); // Uncomment for deep debugging

        if (typeof scriptId !== 'string' || !scriptId.trim()) {
            error('activateTab failed: Invalid scriptId provided (must be a non-empty string).'); return;
        }
        if (!isInitialized) {
            warn(`Cannot activate tab ${scriptId} yet, manager not initialized.`); return;
        }
        if (!registeredTabs.has(scriptId)) {
            error(`activateTab failed: Script ID "${scriptId}" is not registered.`); return;
        }
        if (scriptId === activeStmTabId) {
            // log(`activateTab: Tab ${scriptId} is already active.`);
            return; // Already active, do nothing
        }

        // Deactivate previously active tab (could be native or another STM tab)
        if (activeStmTabId) {
            _deactivateStmTabVisualsAndCallback(activeStmTabId); // Deactivate previous STM tab
        } else {
             // If no STM tab was active, assume a native tab might be. Deactivate native tabs.
             _deactivateNativeTabs();
        }

        // Activate the requested STM tab
        if (_activateStmTabVisualsAndCallback(scriptId)) {
            activeStmTabId = scriptId; // Update the state *after* successful activation attempt
            log(`Programmatically activated tab: ${scriptId}`);
        } else {
            // Activation failed (elements missing?) - logs already happened inside helper.
            warn(`Programmatic activation failed for tab: ${scriptId}. Resetting active tab state.`);
            activeStmTabId = null; // Reset state to avoid inconsistency
        }
    }

    /** @inheritdoc */
    function getPanelElementImpl(scriptId) {
        if (!isInitialized || !panelContainerEl || typeof scriptId !== 'string' || !scriptId.trim()) {
             return null;
        }
        // Use optional chaining for safety, though panelContainerEl is checked above
        return panelContainerEl?.querySelector(`div[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
    }

    /** @inheritdoc */
    function getTabElementImpl(scriptId) {
        if (!isInitialized || !tabContainerEl || typeof scriptId !== 'string' || !scriptId.trim()) {
            return null;
        }
        // Use optional chaining for safety
        return tabContainerEl?.querySelector(`span[${ATTRS.MANAGED}][${ATTRS.SCRIPT_ID}="${scriptId}"]`);
     }

    // --- Global Exposure ---
    /**
     * Exposes the public API on `unsafeWindow` for cross-script communication.
     * `unsafeWindow` bypasses the userscript sandbox, allowing different scripts
     * managed by the same userscript engine instance to access the same object.
     */
    function exposeApiGlobally() {
        log('Attempting to expose API globally on unsafeWindow...');
        try {
            // `unsafeWindow` is provided by some userscript managers (e.g., Tampermonkey, Violentmonkey)
            // It represents the raw page `window` object.
            if (typeof unsafeWindow === 'undefined') {
                throw new Error('`unsafeWindow` is not available. Cannot guarantee cross-script communication.');
            }

            // Check if the API is already defined, possibly by a duplicate script instance.
            if (typeof unsafeWindow.SettingsTabManager !== 'undefined') {
                // Avoid overwriting if it's the exact same object (e.g., script ran twice weirdly but points to same instance)
                // Though realistically, multiple instances would create distinct objects.
                if (unsafeWindow.SettingsTabManager !== publicApi) {
                     warn('`unsafeWindow.SettingsTabManager` is already defined by potentially another script or instance! Overwriting. Check for duplicate STM scripts.');
                } else {
                     warn('`unsafeWindow.SettingsTabManager` seems to be defined already *as this exact object*. This might indicate the script ran multiple times.');
                     // No need to redefine if it's the same object.
                     return;
                }
                // Attempt to remove the old property before redefining. Might fail if non-configurable.
                try {
                    delete unsafeWindow.SettingsTabManager;
                } catch (deleteErr) {
                    warn('Could not delete previous unsafeWindow.SettingsTabManager definition.', deleteErr);
                }
            }

            // Define the property on unsafeWindow.
            Object.defineProperty(unsafeWindow, 'SettingsTabManager', {
                value: publicApi,
                writable: false, // Prevent accidental reassignment by page scripts or basic user errors.
                configurable: true // Allow removal/redefinition by other userscripts or the manager if needed.
            });

            // Verify that the definition succeeded.
            if (unsafeWindow.SettingsTabManager === publicApi) {
                log('SettingsTabManager API exposed successfully on unsafeWindow.');
            } else {
                // This should ideally not happen if defineProperty doesn't throw.
                throw new Error('Failed to verify API definition on unsafeWindow after defineProperty.');
            }

        } catch (e) {
            error('Fatal error exposing SettingsTabManager API:', e);
            // Attempt to alert the user, as dependent scripts will likely fail.
            try {
                 alert(`${MANAGER_ID} Error: Failed to expose API globally on unsafeWindow. Other scripts depending on STM may fail. Check the browser console (F12) for details.`);
            } catch (alertErr) {
                error('Failed to show alert about API exposure failure.');
            }
        }
    }

    exposeApiGlobally(); // Expose the API

})(); // End of IIFE