n8n Workflow Preview Enhancer

Fix the awful non-responsive layout on n8n workflow pages, kupo!

// ==UserScript==
// @name         n8n Workflow Preview Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Fix the awful non-responsive layout on n8n workflow pages, kupo!
// @author       You
// @match        https://n8n.io/workflows/*
// @exclude      https://n8n.io/workflows/
// @exclude      https://n8n.io/workflows
// @grant        none
// @run-at       document-idle
// @license     GPL-3.0-or-later

// ==/UserScript==

(function() {
    'use strict';

    console.log('n8n Layout Fix: Enhanced version with SPA navigation support, kupo!');

    let isFixing = false;
    let fixAttempts = 0;
    const maxFixAttempts = 15; // Increased for slow iframe loading

    // Function to find elements in shadow DOM
    const findInShadowDOM = (selector) => {
        const elements = [];

        // Find all elements with shadow roots
        const allElements = document.querySelectorAll('*');
        allElements.forEach(el => {
            if (el.shadowRoot) {
                const shadowElements = el.shadowRoot.querySelectorAll(selector);
                elements.push(...shadowElements);
            }
        });

        return elements;
    };

    // Enhanced function to wait for iframe content
    const waitForIframeContent = () => {
        return new Promise((resolve) => {
            const checkIframe = () => {
                const iframe = document.querySelector('#int_iframe, iframe.embedded_workflow_iframe');
                if (iframe) {
                    // Check if iframe has loaded content
                    try {
                        if (iframe.contentDocument || iframe.contentWindow) {
                            console.log('Iframe detected and accessible, kupo!');
                            resolve(true);
                            return;
                        }
                    } catch (e) {
                        // Cross-origin iframe, but that's normal for n8n
                        console.log('Cross-origin iframe detected (normal), kupo!');
                        resolve(true);
                        return;
                    }
                }

                // Also check for shadow DOM iframe
                const shadowIframe = findInShadowDOM('#int_iframe, .embedded_workflow_iframe');
                if (shadowIframe.length > 0) {
                    console.log('Shadow DOM iframe found, kupo!');
                    resolve(true);
                    return;
                }

                setTimeout(checkIframe, 200);
            };

            checkIframe();

            // Fallback timeout
            setTimeout(() => resolve(false), 8000);
        });
    };

    // Function to apply all layout fixes
    const fixLayout = async () => {
        if (isFixing) return false;
        isFixing = true;

        console.log(`Fix attempt ${fixAttempts + 1}/${maxFixAttempts}, kupo!`);

        let fixesApplied = 0;

        // Fix 1: Change flex-direction to column
        const targetElement = document.querySelector('.section-content-group.flex.flex-col.gap-8.lg\\:flex-row');
        if (targetElement) {
            targetElement.style.flexDirection = 'column !important';
            targetElement.classList.remove('lg:flex-row');
            console.log('Flex-direction fix applied, kupo!');
            fixesApplied++;
        }

        // Fix 2: Add max-width calc(100vw - 55px) to BOTH outer wrapper variations
        const maxWidthSelectors = [
            'div.max-w-section-default.mx-auto.w-full',
            'div.mx-auto.w-full.max-w-section-default'
        ];

        maxWidthSelectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(el => {
                el.style.maxWidth = 'calc(100vw - 55px)';
                console.log('Max-width calc(100vw - 55px) applied to: ' + selector + ', kupo!');
                fixesApplied++;
            });
        });

        // Fix 3: Update width calc(100vw - 55px) to lg:w-8/12 element
        const targetWidthElement = document.querySelector('div.lg\\:w-8\\/12');
        if (targetWidthElement) {
            targetWidthElement.style.width = 'calc(100vw - 55px)';
            console.log('Width calc(100vw - 55px) applied to lg:w-8/12 element, kupo!');
            fixesApplied++;
        }


        // Wait for iframe content before applying iframe-related fixes
        console.log('Waiting for iframe content to load, kupo...');
        await waitForIframeContent();

        // Fix 4: Regular DOM elements with min-height 100vh
        const regularSelectors = [
            'div.base-frame.relative.rounded-2xl.bg-white.bg-opacity-10.p-2.base-frame--default.workflow-viewer[data-v-57c68cc9][data-v-2f4878b1][data-v-6c0d4504]',
            'div.base-frame-inner.overflow-hidden.rounded-xl[data-v-57c68cc9]',
            'n8n-demo[data-v-2f4878b1]'
        ];

        regularSelectors.forEach(selector => {
            const el = document.querySelector(selector);
            if (el) {
                el.style.minHeight = '100vh';
                console.log('min-height 100vh applied to ' + selector + ', kupo!');
                fixesApplied++;
            }
        });

        // Fix 5: The bg-white div (try different approach)
        const bgWhiteElements = document.querySelectorAll('div[data-v-2f4878b1]');
        bgWhiteElements.forEach(el => {
            if (el.classList.contains('bg-white')) {
                el.style.minHeight = '100vh';
                console.log('min-height 100vh applied to bg-white div, kupo!');
                fixesApplied++;
            }
        });

        // Fix 6: Shadow DOM elements (with retry mechanism)
        const shadowSelectors = [
            '.embedded_workflow',
            '.canvas-container',
            '#int_iframe.embedded_workflow_iframe.non_interactive'
        ];

        shadowSelectors.forEach(selector => {
            const shadowElements = findInShadowDOM(selector);
            shadowElements.forEach(el => {
                el.style.minHeight = '100vh';
                console.log('min-height 100vh applied to shadow DOM element: ' + selector + ', kupo!');
                fixesApplied++;
            });
        });

        // Alternative approach for shadow DOM - find n8n-demo and access its shadow root
        const n8nDemo = document.querySelector('n8n-demo[data-v-2f4878b1]');
        if (n8nDemo && n8nDemo.shadowRoot) {
            const shadowRoot = n8nDemo.shadowRoot;

            // Try to find elements in this specific shadow root
            const embeddedWorkflow = shadowRoot.querySelector('.embedded_workflow');
            if (embeddedWorkflow) {
                embeddedWorkflow.style.minHeight = '100vh';
                console.log('Applied 100vh to .embedded_workflow in shadow root, kupo!');
                fixesApplied++;
            }

            const canvasContainer = shadowRoot.querySelector('.canvas-container');
            if (canvasContainer) {
                canvasContainer.style.minHeight = '100vh';
                console.log('Applied 100vh to .canvas-container in shadow root, kupo!');
                fixesApplied++;
            }

            const iframe = shadowRoot.querySelector('#int_iframe');
            if (iframe) {
                iframe.style.minHeight = '100vh';
                console.log('Applied 100vh to iframe in shadow root, kupo!');
                fixesApplied++;
            }
        }

        isFixing = false;
        return fixesApplied > 0;
    };

    // Function to detect URL changes (for SPA navigation)
    const detectURLChange = () => {
        let currentURL = window.location.href;

        const checkURLChange = () => {
            if (window.location.href !== currentURL) {
                currentURL = window.location.href;
                console.log('URL changed to:', currentURL, 'kupo!');

                // Reset fix attempts for new page
                fixAttempts = 0;

                // Wait a bit for the new content to start loading
                setTimeout(() => {
                    console.log('Starting layout fixes after navigation, kupo!');
                    startFixingProcess();
                }, 500);
            }
        };

        setInterval(checkURLChange, 100);
    };

    // Main fixing process with enhanced retry logic
    const startFixingProcess = async () => {
        // Try to fix immediately
        if (await fixLayout()) {
            console.log('Initial layout fixes applied successfully, kupo!');
        }

        // Enhanced retry mechanism
        const retryInterval = setInterval(async () => {
            fixAttempts++;

            if (fixAttempts >= maxFixAttempts) {
                clearInterval(retryInterval);
                console.log('Max fix attempts reached. Layout fix monitoring complete, kupo!');
                return;
            }

            await fixLayout();
        }, 1000); // Check every second

        // Watch for dynamic content changes
        const observer = new MutationObserver(async (mutations) => {
            let shouldCheck = false;

            mutations.forEach(mutation => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // Check if any added nodes contain our target elements
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) { // Element node
                            if (node.matches && (
                                node.matches('n8n-demo') ||
                                node.matches('.base-frame') ||
                                node.matches('iframe') ||
                                node.querySelector && (
                                    node.querySelector('n8n-demo') ||
                                    node.querySelector('.base-frame') ||
                                    node.querySelector('iframe')
                                )
                            )) {
                                shouldCheck = true;
                            }
                        }
                    });
                }
            });

            if (shouldCheck && fixAttempts < maxFixAttempts) {
                setTimeout(async () => {
                    console.log('DOM change detected, applying fixes, kupo!');
                    await fixLayout();
                }, 300);
            }
        });

        // Start observing with enhanced options
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            attributes: false, // Don't watch attributes to reduce noise
            attributeOldValue: false
        });
    };

    // Start the URL change detection for SPA navigation
    detectURLChange();

    // Start the initial fixing process
    startFixingProcess();

})();