Bubble Editor Enhancements

Enhances Bubble.io editor: Data Type/Option Sets/Attributes (layout, colors). Modal dialogs - full keyboard support (Enter/Esc, Tab nav). Toggleable canvas padding. Ctrl+N for hinted "Create a new..." buttons.

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         Bubble Editor Enhancements
// @namespace    http://tampermonkey.net/
// @version      2025.05.10-04
// @description  Enhances Bubble.io editor: Data Type/Option Sets/Attributes (layout, colors). Modal dialogs - full keyboard support (Enter/Esc, Tab nav). Toggleable canvas padding. Ctrl+N for hinted "Create a new..." buttons.
// @author       [email protected]
// @match        https://*bubble.io/page*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bubble.io
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    const SCRIPT_NAME = "Bubble Enhancements";
    const DEBUG_MODE = false;

    const CANVAS_PADDING_ENABLED_KEY = "bubbleEnhancements_canvasPaddingEnabled";
    let isCanvasPaddingEnabled = true;
    let canvasPaddingMenuCommandId = null;

    const NAME_INPUT_SELECTOR_IN_MODAL = '.new-field-name-wrapper input.new-field-name.bubble-ui';
    const TYPE_DROPDOWN_SELECTOR_IN_MODAL = '.editor.dropdown.new-field-type .property-editor-control.composer-container';
    const DROPDOWN_CAPTION_SELECTOR = '.dropdown-caption-container';
    const DROPDOWN_CONTAINER_ACTUAL_SELECTOR = '.dropdown-container.new-composer';
    const CHECKBOX_SELECTOR_IN_MODAL = '.editor.checkbox .new-composer.composer-checkbox .component-checkbox';
    const MODAL_CONTENT_SELECTOR = '.popup-content';
    const CREATE_NEW_BUTTON_SELECTOR = '.btn.add-new-type';

    function logDebug(message, ...args) { if (DEBUG_MODE) { console.log(`[${SCRIPT_NAME}] DEBUG:`, message, ...args); } }
    function logInfo(message, ...args) { console.log(`[${SCRIPT_NAME}] INFO:`, message, ...args); }
    function logError(message, ...args) { console.error(`[${SCRIPT_NAME}] ERROR:`, message, ...args); }

    function addGlobalStyle(css) {
        const head = document.getElementsByTagName('head')[0];
        if (!head) { logError("Could not find <head> to inject CSS."); return; }
        const style = document.createElement('style');
        style.type = 'text/css'; style.innerHTML = css; head.appendChild(style);
        logDebug("Custom CSS injected.");
    }

    function applyCanvasPaddingState(enabled) { /* ... (no change) ... */
        if (enabled) document.body.classList.add('canvas-padding-active');
        else document.body.classList.remove('canvas-padding-active');
        logDebug(`Canvas padding ${enabled ? 'enabled' : 'disabled'}.`);
    }
    function toggleCanvasPadding() { /* ... (no change) ... */
        isCanvasPaddingEnabled = !isCanvasPaddingEnabled;
        GM_setValue(CANVAS_PADDING_ENABLED_KEY, isCanvasPaddingEnabled);
        applyCanvasPaddingState(isCanvasPaddingEnabled);
        registerMenuCommands();
        logInfo(`Canvas padding toggled ${isCanvasPaddingEnabled ? 'ON' : 'OFF'}.`);
    }
    function registerMenuCommands() { /* ... (no change) ... */
        if (canvasPaddingMenuCommandId !== null) {
            try { GM_unregisterMenuCommand(canvasPaddingMenuCommandId); } catch (e) { logError("Error unregistering menu command:", e); }
        }
        canvasPaddingMenuCommandId = GM_registerMenuCommand(
            `Toggle Canvas Right Padding (Currently: ${isCanvasPaddingEnabled ? 'ON' : 'OFF'})`,
            toggleCanvasPadding, 'c'
        );
        logDebug("Registered menu command:", canvasPaddingMenuCommandId);
    }
    function loadSettings() { /* ... (no change) ... */
        isCanvasPaddingEnabled = GM_getValue(CANVAS_PADDING_ENABLED_KEY, true);
        applyCanvasPaddingState(isCanvasPaddingEnabled);
    }

    loadSettings();
    registerMenuCommands();

    addGlobalStyle(`
        body.canvas-padding-active .canvas-inner { justify-self: end !important; padding-right: 1%; }
        /* a.logo { visibility: hidden; } */
        .field-name { width: 50%; min-width: 250px; display: flex; }
        .field-name > div.composer-textbox { flex: 1 1 auto; min-width: 168px; }
        .field-name input { width: 100% !important; min-width: 168px; }
        .custom-fields:not(.option-set-attributes) > div.field:not(.built-in) { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 6px; box-sizing: border-box; }
        .custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .new-composer.composer-textbox:first-child { width: 34%; min-width: 180px; flex-shrink: 0; }
        .custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .new-composer.composer-textbox:first-child input { width: 100%; box-sizing: border-box; }
        .custom-fields > div.field > .field-type { flex-shrink: 2; text-align: left; min-width: 0; }
        .custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .field-default-caption { width: 60px; flex-shrink: 0; text-align: center; }
        .custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .composer-dropdown.bubble-ui > .spot { width: 100%; box-sizing: border-box; }
        .custom-fields > div.field > .delete-btn { order: 2; flex-shrink: 0; }
        .custom-fields > div.field > .comment-btn { flex-shrink: 0; margin-left: auto; }
        .custom-fields > div.field:not(.built-in):nth-child(even) { background-color: #f7f7f7; }
        .custom-fields > div.field:not(.built-in):nth-child(odd) { background-color: transparent; }
        .custom-fields .built-in-fields { margin-top: 10px; }
        .custom-fields .built-in-fields > div.field.built-in { display: flex; align-items: center; gap: 12px; padding: 8px 6px; border-top: 1px solid #eee; }
        .field:not(.built-in) > .field-default-caption ~ input { flex-grow:1; width: auto !important; min-width:0; }
        .field:not(.built-in) > .field-default-caption ~ .composer-dropdown,
        .field:not(.built-in) > .field-default-caption ~ .composer-textbox{ flex-grow:1; }
        .field:not(.built-in) > .field-default-caption ~ .composer-image,
        .field:not(.built-in) > .field-default-caption ~ .composer-file{ margin-right: auto; }
        .composer-textbox > input { width: 100% !important; }
        .custom-fields .built-in-fields > div.field.built-in .built-in-field-name { width: 33%; min-width: 150px; flex-shrink: 0; }
        .custom-fields .built-in-fields > div.field.built-in .built-in-mention { margin-right: auto; white-space: nowrap; flex-shrink: 0; }
        .custom-fields .built-in-fields > div.field.built-in:nth-child(even) { background-color: #fafafa; }
        .custom-fields .built-in-fields > div.field.built-in:nth-child(odd) { background-color: transparent; }
        .custom-fields.option-set-attributes > div.field:not(.built-in) { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 6px; box-sizing: border-box; }
        .custom-fields.option-set-attributes > div.field:not(.built-in) > .new-composer.composer-textbox:first-child { width: 34%; min-width: 180px; flex-shrink: 0; }
        .custom-fields.option-set-attributes > div.field:not(.built-in) > .new-composer.composer-textbox:first-child input { width: 100%; box-sizing: border-box; }
        .key-hint { text-decoration: underline; }
    `);

    function findTargetableCreateNewButton() { // Renamed for clarity
        const buttons = document.querySelectorAll(
            `${CREATE_NEW_BUTTON_SELECTOR}[data-key-hint-target="true"]` // Look for buttons marked as targets
        );
        for (const btn of buttons) {
            if (btn.offsetParent !== null &&
                getComputedStyle(btn).display !== 'none' &&
                getComputedStyle(btn).visibility !== 'hidden' &&
                !btn.closest('.bottom-stripe, .bottom-popup-row, .popup-content .children')) {
                logDebug("Found targetable 'Create New...' button:", btn, btn.textContent);
                return btn;
            }
        }
        logDebug("No targetable 'Create New...' button found.");
        return null;
    }

    function styleCreateNewButton(button, keyChar = 'N') {
        if (!button || button.dataset.keyHintAdded === 'true') return; // Already processed

        button.dataset.keyHintAdded = 'true'; // Mark as processed to avoid re-evaluating innerHTML
        const originalText = button.textContent || button.innerText || "";
        let newHtml = originalText;
        let hintAppliedSuccessfully = false;
        // We only want to target buttons that are clearly for "creating something NEW"
        // Check if the text contains "new" or "create a new"
        const isTargetContext = /new|create a new/i.test(originalText);

        if (isTargetContext) {
            // Try to find the keyChar (case-insensitive) within "New" or "Create" context
            const contextPattern = new RegExp(`(new|create)([^${keyChar}]*?)(${keyChar})`, 'i');
            let match = originalText.match(contextPattern);

            if (match && match[3]) {
                const charIndex = originalText.toLowerCase().indexOf(match[3].toLowerCase(), match.index + match[1].length + (match[2] ? match[2].length : 0) );
                if (charIndex > -1) {
                    newHtml = originalText.substring(0, charIndex) +
                              `<span class="key-hint">${originalText[charIndex]}</span>` +
                              originalText.substring(charIndex + 1);
                    hintAppliedSuccessfully = true;
                }
            } else { // Fallback: if "new" or "create a new" is present, but the specific pattern above didn't match 'N' within it,
                     // try to find the first 'N' in the whole string if the context is right.
                const firstNIndex = originalText.toUpperCase().indexOf("N");
                if (firstNIndex > -1) {
                     newHtml = originalText.substring(0, firstNIndex) +
                              `<span class="key-hint">${originalText[firstNIndex]}</span>` +
                              originalText.substring(firstNIndex + 1);
                     hintAppliedSuccessfully = true;
                }
            }

            if (hintAppliedSuccessfully) {
                button.innerHTML = newHtml;
                button.dataset.keyHintTarget = 'true'; // Mark as a valid target for Ctrl+N
                logDebug("Added key hint and marked as target:", button.textContent);
            } else {
                // Text contains "new" or "create a new", but 'N' wasn't found to underline.
                // Do not mark as keyHintTarget. It's a .btn.add-new-type but not one we can visually hint for 'N'.
                logDebug("Button has target context but 'N' not found for hinting:", button.textContent);
            }
        } else {
            // Button has class .btn.add-new-type, but text doesn't match "new" or "create a new"
            // So, we don't consider it a target for this specific Ctrl+N functionality.
            logDebug("Button class matches, but text context not for 'Create a new...':", button.textContent);
            // No dataset.keyHintTarget = 'true' is set.
        }
    }

    function handleGlobalKeyDown(event) {
        const activeElement = document.activeElement;
        const isInputFocused = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);

        if (event.ctrlKey && event.key.toLowerCase() === 'n') {
            if (isInputFocused) {
                logDebug("Ctrl+N: Input focused, allowing browser default.");
                return;
            }
            const anyModalOpen = document.querySelector(`${MODAL_CONTENT_SELECTOR}:not([style*="display: none"]):not([style*="visibility: hidden"]), .overlay:not([style*="display: none"]):not([style*="visibility: hidden"])`);
            if (anyModalOpen && anyModalOpen.offsetParent !== null) {
                logDebug("Ctrl+N: A modal is already open, ignoring.", anyModalOpen);
                return;
            }

            const buttonToClick = findTargetableCreateNewButton(); // Now looks for hinted targets
            if (buttonToClick) {
                logInfo("Ctrl+N: Clicking hinted 'Create New...' button:", buttonToClick.textContent);
                buttonToClick.click();
                event.preventDefault();
                event.stopPropagation();
            } else {
                logDebug("Ctrl+N: No suitable hinted 'Create New...' button found.");
            }
            return;
        }

        if (event.key !== 'Enter' && event.key !== 'Escape') return;
        // ... (rest of modal Enter/Escape logic remains unchanged) ...
        const activeModals = Array.from(document.querySelectorAll(MODAL_CONTENT_SELECTOR))
            .filter(el => el.offsetParent !== null && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden');

        if (activeModals.length === 0) return;
        const currentModal = activeModals[activeModals.length - 1];

        if (event.key === 'Enter' && activeElement &&
            (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable) &&
            currentModal.contains(activeElement) &&
            !activeElement.closest('.bottom-stripe, .bottom-popup-row')) {
            const associatedDropdown = activeElement.closest('.composer-dropdown');
            const isAssociatedDropdownOpen = associatedDropdown && associatedDropdown.querySelector('.dropdown-container.opened');
            if (!isAssociatedDropdownOpen) {
                logDebug("Modal Enter in input/textarea (not in footer, its dropdown not open). Allowing default.");
                return;
            }
        }

        if (event.key === 'Escape') {
            const openDropdownInModal = currentModal.querySelector(DROPDOWN_CONTAINER_ACTUAL_SELECTOR + '.opened');
            if (openDropdownInModal &&
                (openDropdownInModal.contains(activeElement) || activeElement?.closest(DROPDOWN_CONTAINER_ACTUAL_SELECTOR + '.opened') === openDropdownInModal)) {
                logDebug("Modal Esc: Active element is within an open dropdown. Allowing local/nav handler.");
                return;
            }
        }

        const buttonSearchScope = currentModal;
        let buttonToClickModal = null;
        let buttonSelectorModal = '';
        const modalTitle = currentModal.querySelector('.popup-title')?.textContent.trim() || 'Unknown Modal';

        if (event.key === 'Enter') {
            const selectors = [
                '.btn.btn-create.bubble-ui:not(.disabled)', '.btn.btn-primary:not(.disabled)', '.btn.btn-create:not(.disabled)',
                '.bubble-button.primary:not(.disabled)', '.bubble-element.Button[class*="primary"]:not(.disabled)'
            ];
            for (const sel of selectors) {
                buttonToClickModal = buttonSearchScope.querySelector(sel);
                if (buttonToClickModal && buttonToClickModal.offsetParent !== null && getComputedStyle(buttonToClickModal).display !== 'none' && getComputedStyle(buttonToClickModal).visibility !== 'hidden') {
                    buttonSelectorModal = sel; break;
                }
                buttonToClickModal = null;
            }
            if (!buttonToClickModal) {
                const allButtons = buttonSearchScope.querySelectorAll('.btn:not(.disabled), .bubble-element.Button:not(.disabled)');
                buttonToClickModal = Array.from(allButtons).find(btn =>
                    !btn.classList.contains('btn-cancel') && !(btn.classList.contains('cancel')) &&
                    !(btn.textContent || '').toLowerCase().includes('cancel') &&
                    btn.offsetParent !== null && getComputedStyle(btn).display !== 'none' && getComputedStyle(btn).visibility !== 'hidden'
                );
                if (buttonToClickModal) buttonSelectorModal = "Fallback: first visible non-cancel button";
            }
        } else if (event.key === 'Escape') {
            const selectors = [
                '.btn.btn-cancel:not(.disabled)', '.btn[class*="cancel"]:not(.disabled)',
                '.bubble-button.cancel:not(.disabled)', '.bubble-element.Button[class*="cancel"]:not(.disabled)'
            ];
            for (const sel of selectors) {
                buttonToClickModal = buttonSearchScope.querySelector(sel);
                if (buttonToClickModal && buttonToClickModal.offsetParent !== null && getComputedStyle(buttonToClickModal).display !== 'none' && getComputedStyle(buttonToClickModal).visibility !== 'hidden') {
                    buttonSelectorModal = sel; break;
                }
                buttonToClickModal = null;
            }
            if (!buttonToClickModal) {
                const allButtons = buttonSearchScope.querySelectorAll('.btn:not(.disabled), .bubble-element.Button:not(.disabled)');
                buttonToClickModal = Array.from(allButtons).find(btn =>
                    ((btn.textContent || '').toLowerCase().includes('cancel') || btn.classList.contains('cancel')) &&
                    btn.offsetParent !== null && getComputedStyle(btn).display !== 'none' && getComputedStyle(btn).visibility !== 'hidden'
                );
                if (buttonToClickModal) buttonSelectorModal = "Fallback: first visible button with 'cancel' in text or class";
            }
        }

        if (buttonToClickModal) {
            logDebug(`Modal Click: '${buttonToClickModal.textContent.trim()}' (sel: ${buttonSelectorModal}) in modal ("${modalTitle}") via ${event.key}.`);
            buttonToClickModal.click();
            event.preventDefault();
            event.stopPropagation();
        } else {
            if (DEBUG_MODE) {
                 logDebug(`Modal Click: No suitable button for ${event.key} in modal ("${modalTitle}").`);
            }
        }
    }
    document.addEventListener('keydown', handleGlobalKeyDown, true);

    function dispatchMouseEvents(element) { /* ... (no change) ... */
        if (!element) { logError("dispatchMouseEvents: element is null"); return false; }
        logDebug("dispatchMouseEvents on:", element);
        try {
            const downEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
            const upEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
            const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
            element.dispatchEvent(downEvent);
            element.dispatchEvent(upEvent);
            element.dispatchEvent(clickEvent);
            return true;
        } catch (e) { logError("dispatchMouseEvents Error:", e); return false; }
    }
    function standardClick(element) { /* ... (no change) ... */
        if (!element) { logError("standardClick: element is null"); return false; }
        logDebug("standardClick on:", element);
        try { element.click(); return true;
        } catch (e) { logError("standardClick Error:", e); return false; }
    }
    function isDropdownOpen(dropdownTriggerElement) { /* ... (no change) ... */
        if (!dropdownTriggerElement) return false;
        const actualDropdownContainer = dropdownTriggerElement.querySelector(DROPDOWN_CONTAINER_ACTUAL_SELECTOR);
        return actualDropdownContainer ? actualDropdownContainer.classList.contains('opened') : false;
    }
    function handleGenericFieldAttributeModal(modalElement) { /* ... (no change) ... */
        const nameInput = modalElement.querySelector(NAME_INPUT_SELECTOR_IN_MODAL);
        const typeDropdown = modalElement.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL);
        const listCheckbox = modalElement.querySelector(CHECKBOX_SELECTOR_IN_MODAL);
        if (nameInput) { if (document.activeElement !== nameInput && !nameInput.contains(document.activeElement)) { nameInput.focus(); } nameInput.removeEventListener('keydown', handleModalNavKeyPress); nameInput.addEventListener('keydown', handleModalNavKeyPress); }
        if (typeDropdown) { typeDropdown.removeEventListener('keydown', handleModalNavKeyPress); typeDropdown.addEventListener('keydown', handleModalNavKeyPress); }
        if (listCheckbox) { listCheckbox.removeEventListener('keydown', handleModalNavKeyPress); listCheckbox.addEventListener('keydown', handleModalNavKeyPress); }
    }
    function handleModalNavKeyPress(event) { /* ... (no change, ensure Esc propagation stop for dropdowns) ... */
        const { key, target, shiftKey } = event;
        const modalElement = target.closest(MODAL_CONTENT_SELECTOR);
        if (!modalElement) return;

        const nameInput = modalElement.querySelector(NAME_INPUT_SELECTOR_IN_MODAL);
        const typeDropdown = modalElement.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL);
        const listCheckbox = modalElement.querySelector(CHECKBOX_SELECTOR_IN_MODAL);

        if (!(nameInput && typeDropdown && listCheckbox)) {
            return;
        }

        if (key === 'Tab') {
            if (target === nameInput && !shiftKey) {
                logDebug("Nav Tab from Name Input"); event.preventDefault();
                if (typeDropdown) {
                    typeDropdown.focus(); logDebug("Nav Focused Type Dropdown.");
                    if (!isDropdownOpen(typeDropdown)) {
                        logDebug("Nav Attempting to open dropdown..."); standardClick(typeDropdown);
                        const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
                        if (captionContainer) { standardClick(captionContainer); dispatchMouseEvents(captionContainer); logDebug("Nav Dropdown open sequence complete on caption.");
                        } else { logError("Nav Caption container not found. Fallback: dispatch to main trigger."); dispatchMouseEvents(typeDropdown); }
                    }
                } else if (listCheckbox) { listCheckbox.focus(); }
            } else if (target === typeDropdown && !shiftKey) {
                logDebug("Nav Tab from Type Dropdown"); event.preventDefault();
                if (isDropdownOpen(typeDropdown)) {
                    logDebug("Nav Attempting to close dropdown...");
                    const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
                    if (captionContainer) dispatchMouseEvents(captionContainer); else dispatchMouseEvents(typeDropdown);
                    logDebug("Nav Dropdown close attempt complete.");
                }
                if (listCheckbox) { listCheckbox.focus(); logDebug("Nav Focused List Checkbox."); }
            } else if (target === listCheckbox && !shiftKey) { logDebug("Nav Tab from list checkbox, allowing default.");
            } else if (target === listCheckbox && shiftKey) {
                logDebug("Nav Shift+Tab from list checkbox."); event.preventDefault();
                if (typeDropdown) typeDropdown.focus(); else if (nameInput) nameInput.focus();
            } else if (target === typeDropdown && shiftKey) {
                logDebug("Nav Shift+Tab from type dropdown."); event.preventDefault();
                if (isDropdownOpen(typeDropdown)) {
                    const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
                    if (captionContainer) dispatchMouseEvents(captionContainer); else dispatchMouseEvents(typeDropdown);
                }
                if (nameInput) nameInput.focus();
            }
        }
        else if (key === ' ' && target === listCheckbox) {
            logDebug("Nav Space on List Checkbox."); event.preventDefault(); standardClick(target); logDebug("Nav Clicked Checkbox via Space.");
        }
        else if (key === 'Escape' && target.closest(TYPE_DROPDOWN_SELECTOR_IN_MODAL)) {
            if (typeDropdown && isDropdownOpen(typeDropdown)) {
                logDebug("Nav Escape on/in open dropdown: attempting to close.");
                const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
                if (captionContainer) dispatchMouseEvents(captionContainer); else dispatchMouseEvents(typeDropdown);
                typeDropdown.focus();
                event.preventDefault();
                event.stopPropagation(); // Crucial
                logDebug("Nav Closed dropdown via Escape, refocused trigger. Propagation stopped.");
            }
        }
    }

    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            // Modal processing (no change from previous versions)
            if (mutation.type === 'childList' || (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class'))) {
                 const addedNodesForModals = mutation.type === 'childList' ? Array.from(mutation.addedNodes) : (mutation.target.nodeType === 1 ? [mutation.target] : []);
                 for (const node of addedNodesForModals) { /* ... modal processing as before ... */
                    if (node.nodeType !== Node.ELEMENT_NODE) continue;
                    const modals = [];
                    if (node.matches && node.matches(MODAL_CONTENT_SELECTOR)) modals.push(node);
                    else if (node.querySelectorAll) modals.push(...node.querySelectorAll(MODAL_CONTENT_SELECTOR));
                    for (const modal of modals) {
                        const isGenericFieldModal = modal.querySelector(NAME_INPUT_SELECTOR_IN_MODAL) && modal.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL);
                        if (isGenericFieldModal) {
                            if (modal.offsetParent !== null && getComputedStyle(modal).display !== 'none' && getComputedStyle(modal).visibility !== 'hidden') {
                                if (!modal.dataset.tampermonkeyFieldModalProcessed) {
                                    if(DEBUG_MODE) { /* ... */ }
                                    modal.dataset.tampermonkeyFieldModalProcessed = 'true';
                                    handleGenericFieldAttributeModal(modal);
                                }
                            } else {
                                if (modal.dataset.tampermonkeyFieldModalProcessed) {
                                     if(DEBUG_MODE) { /* ... */ }
                                    delete modal.dataset.tampermonkeyFieldModalProcessed;
                                    modal.querySelector(NAME_INPUT_SELECTOR_IN_MODAL)?.removeEventListener('keydown', handleModalNavKeyPress);
                                    modal.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL)?.removeEventListener('keydown', handleModalNavKeyPress);
                                    modal.querySelector(CHECKBOX_SELECTOR_IN_MODAL)?.removeEventListener('keydown', handleModalNavKeyPress);
                                }
                            }
                        }
                    }
                }
            }

            // Create New Button Styling
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches && node.matches(CREATE_NEW_BUTTON_SELECTOR)) {
                            styleCreateNewButton(node);
                        }
                        node.querySelectorAll(CREATE_NEW_BUTTON_SELECTOR).forEach(styleCreateNewButton);
                    }
                });
            } else if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) {
                if (mutation.target.nodeType === Node.ELEMENT_NODE && mutation.target.matches(CREATE_NEW_BUTTON_SELECTOR)) {
                    // Re-evaluate styling if style/class changes might affect visibility or if it wasn't hinted yet
                    if (mutation.target.offsetParent !== null && getComputedStyle(mutation.target).display !== 'none') {
                         // Only call if not already hinted, or if you want to re-evaluate context (though current logic prevents re-innerHTML)
                        if (mutation.target.dataset.keyHintAdded !== 'true') {
                            styleCreateNewButton(mutation.target);
                        }
                    }
                }
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });

    function initialButtonScan() {
        logDebug("Performing initial scan for '.btn.add-new-type' buttons to style.");
        document.querySelectorAll(CREATE_NEW_BUTTON_SELECTOR).forEach(btn => {
            if (btn.offsetParent !== null && getComputedStyle(btn).display !== 'none') {
                styleCreateNewButton(btn);
            }
        });
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialButtonScan);
    } else {
        initialButtonScan();
    }

    logInfo(`Bubble.io enhancements script loaded. (v${GM_info.script.version}) - DEBUG_MODE is ${DEBUG_MODE ? 'ON' : 'OFF'}. Canvas padding is ${isCanvasPaddingEnabled ? 'ON' : 'OFF'}.`);
})();