Greasy Fork is available in English.
Shared UI components and styling for AO3 userscripts
当前为 
        此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/552743/1678334/AO3%3A%20Menu%20Helpers%20Library.js
      
    // ==UserScript==
    // @name         AO3: Menu Helpers Library
    // @namespace    http://tampermonkey.net/
    // @version      1.0.2
    // @description  Shared UI components and styling for AO3 userscripts
    // @author       BlackBatCat
    // @license      MIT
    // @grant        none
    // ==/UserScript==
     
    (function() {
      'use strict';
      
      // Prevent multiple injections
      if (window.AO3MenuHelpers) {
        console.log('[AO3 Menu Helpers] Library already loaded, version', window.AO3MenuHelpers.version);
        return;
      }
      
      // Cache for background color to avoid repeated DOM operations
      let cachedInputBg = null;
      let stylesInjected = false;
      
      window.AO3MenuHelpers = {
        version: '1.0.2',
        
        /**
         * Detects AO3's input field background color from current theme
         * Uses caching to avoid repeated DOM operations
         * @returns {string} Background color (hex or rgba format)
         */
        getAO3InputBackground() {
          if (cachedInputBg) return cachedInputBg;
          
          let inputBg = '#fffaf5'; // Fallback default
          const testInput = document.createElement('input');
          document.body.appendChild(testInput);
          
          try {
            const computedStyle = window.getComputedStyle(testInput);
            const computedBg = computedStyle.backgroundColor;
            if (computedBg && computedBg !== 'rgba(0, 0, 0, 0)' && computedBg !== 'transparent') {
              inputBg = computedBg;
            }
          } catch (e) {
            console.warn('[AO3 Menu Helpers] Failed to detect background color:', e);
          } finally {
            testInput.remove();
          }
          
          cachedInputBg = inputBg;
          return inputBg;
        },
        
        /**
         * Injects shared CSS styles for all menu components
         * Only injects once per page load, safe to call multiple times
         * Automatically called when library loads
         */
        injectSharedStyles() {
          if (stylesInjected) return;
          if (!document.head) {
            console.warn('[AO3 Menu Helpers] Cannot inject styles: document.head not available');
            return;
          }
          
          const existingStyle = document.getElementById('ao3-menu-helpers-styles');
          if (existingStyle) {
            stylesInjected = true;
            return;
          }
          
          const inputBg = this.getAO3InputBackground();
          
          const style = document.createElement('style');
          style.id = 'ao3-menu-helpers-styles';
          style.textContent = `
            /* Dialog Container */
            .ao3-menu-dialog {
              position: fixed;
              top: 50%;
              left: 50%;
              transform: translate(-50%, -50%);
              background: ${inputBg};
              padding: 20px;
              border-radius: 8px;
              box-shadow: 0 0 20px rgba(0,0,0,0.2);
              z-index: 10000;
              width: 90%;
              max-width: 600px;
              max-height: 80vh;
              overflow-y: auto;
              font-family: inherit;
              font-size: inherit;
              color: inherit;
              box-sizing: border-box;
            }
            
            .ao3-menu-dialog h3 {
              text-align: center;
              margin-top: 0;
              color: inherit;
              font-family: inherit;
            }
            
            /* Settings Sections */
            .ao3-menu-dialog .settings-section {
              background: rgba(0,0,0,0.03);
              border-radius: 6px;
              padding: 15px;
              margin-bottom: 20px;
              border-left: 4px solid currentColor;
            }
            
            .ao3-menu-dialog .section-title {
              margin-top: 0;
              margin-bottom: 15px;
              font-size: 1.2em;
              font-weight: bold;
              color: inherit;
              opacity: 0.85;
              font-family: inherit;
            }
            
            /* Setting Groups */
            .ao3-menu-dialog .setting-group {
              margin-bottom: 15px;
            }
            
            .ao3-menu-dialog .setting-label {
              display: block;
              margin-bottom: 6px;
              font-weight: bold;
              color: inherit;
              opacity: 0.9;
            }
            
            .ao3-menu-dialog .setting-description {
              display: block;
              margin-bottom: 8px;
              font-size: 0.9em;
              color: inherit;
              opacity: 0.6;
              line-height: 1.4;
            }
            
            /* Checkbox and Radio Labels */
            .ao3-menu-dialog .checkbox-label {
              display: block;
              font-weight: normal;
              color: inherit;
              margin-bottom: 8px;
            }
            
            .ao3-menu-dialog .radio-label {
              display: block;
              font-weight: normal;
              color: inherit;
              margin-left: 20px;
              margin-bottom: 8px;
            }
            
            /* Subsettings (indented settings) */
            .ao3-menu-dialog .subsettings {
              padding-left: 20px;
              margin-top: 10px;
            }
            
            /* Layout Helpers */
            .ao3-menu-dialog .two-column {
              display: grid;
              grid-template-columns: 1fr 1fr;
              gap: 15px;
            }
            
            .ao3-menu-dialog .setting-group + .two-column {
              margin-top: 15px;
            }
            
            /* Slider with Value Display */
            .ao3-menu-dialog .slider-with-value {
              display: flex;
              align-items: center;
              gap: 10px;
            }
            
            .ao3-menu-dialog .slider-with-value input[type="range"] {
              flex-grow: 1;
            }
            
            .ao3-menu-dialog .value-display {
              min-width: 40px;
              text-align: center;
              font-weight: bold;
              color: inherit;
              opacity: 0.6;
            }
            
            /* Form Inputs */
            .ao3-menu-dialog input[type="text"],
            .ao3-menu-dialog input[type="number"],
            .ao3-menu-dialog input[type="color"],
            .ao3-menu-dialog select,
            .ao3-menu-dialog textarea {
              width: 100%;
              box-sizing: border-box;
            }
            
            .ao3-menu-dialog textarea {
              min-height: 100px;
              resize: vertical;
              font-family: inherit;
            }
            
            .ao3-menu-dialog input[type="text"]:focus,
            .ao3-menu-dialog input[type="number"]:focus,
            .ao3-menu-dialog input[type="color"]:focus,
            .ao3-menu-dialog select:focus,
            .ao3-menu-dialog textarea:focus {
              background: ${inputBg} !important;
            }
            
            .ao3-menu-dialog input::placeholder,
            .ao3-menu-dialog textarea::placeholder {
              opacity: 0.6 !important;
            }
            
            /* Buttons */
            .ao3-menu-dialog .button-group {
              display: flex;
              justify-content: space-between;
              gap: 10px;
              margin-top: 20px;
            }
            
            .ao3-menu-dialog .button-group button {
              flex: 1;
              padding: 10px;
              color: inherit;
              opacity: 0.9;
            }
            
            /* Reset Link */
            .ao3-menu-dialog .reset-link {
              text-align: center;
              margin-top: 10px;
              color: inherit;
              opacity: 0.7;
            }
            
            /* Tooltips */
            .ao3-menu-dialog .symbol.question {
              font-size: 0.5em;
              vertical-align: middle;
            }
            
            /* Keyboard key styling */
            .ao3-menu-dialog kbd {
              padding: 2px 6px;
              background: rgba(0,0,0,0.1);
              border-radius: 3px;
              font-family: monospace;
              font-size: 0.9em;
            }
          `;
          
          document.head.appendChild(style);
          stylesInjected = true;
          console.log('[AO3 Menu Helpers] Styles injected');
        },
        
        /**
         * Creates a dialog/popup container
         * @param {string} title - Dialog title (can include emoji)
         * @param {Object} [options={}] - Optional configuration
         * @param {string} [options.width='90%'] - Dialog width
         * @param {string} [options.maxWidth='600px'] - Maximum dialog width
         * @param {string} [options.maxHeight='80vh'] - Maximum dialog height
         * @param {string} [options.className=''] - Additional CSS classes
         * @returns {HTMLElement} Dialog container element
         */
        createDialog(title, options = {}) {
          const {
            width = '90%',
            maxWidth = '600px',
            maxHeight = '80vh',
            className = ''
          } = options;
          
          const dialog = document.createElement('div');
          dialog.className = `ao3-menu-dialog ${className}`.trim();
          
          if (width !== '90%') dialog.style.width = width;
          if (maxWidth !== '600px') dialog.style.maxWidth = maxWidth;
          if (maxHeight !== '80vh') dialog.style.maxHeight = maxHeight;
          
          const titleElement = document.createElement('h3');
          titleElement.textContent = title;
          dialog.appendChild(titleElement);
          
          return dialog;
        },
        
        /**
         * Creates a settings section with colored border
         * @param {string} title - Section title
         * @param {string|HTMLElement} [content=''] - Section content (HTML string or element)
         * @returns {HTMLElement} Section container
         */
        createSection(title, content = '') {
          const section = document.createElement('div');
          section.className = 'settings-section';
          
          const titleElement = document.createElement('h4');
          titleElement.className = 'section-title';
          titleElement.textContent = title;
          section.appendChild(titleElement);
          
          if (typeof content === 'string' && content) {
            section.innerHTML += content;
          } else if (content instanceof HTMLElement) {
            section.appendChild(content);
          }
          
          return section;
        },
        
        /**
         * Creates a setting group container
         * @param {string|HTMLElement} content - Group content
         * @returns {HTMLElement} Setting group div
         */
        createSettingGroup(content = '') {
          const group = document.createElement('div');
          group.className = 'setting-group';
          
          if (typeof content === 'string' && content) {
            group.innerHTML = content;
          } else if (content instanceof HTMLElement) {
            group.appendChild(content);
          }
          
          return group;
        },
        
        /**
         * Creates a tooltip help icon
         * @param {string} text - Tooltip text
         * @returns {HTMLElement} Tooltip span element
         */
        createTooltip(text) {
          if (!text) return document.createTextNode('');
          
          const tooltip = document.createElement('span');
          tooltip.className = 'symbol question';
          tooltip.title = text;
          
          const questionMark = document.createElement('span');
          questionMark.textContent = '?';
          tooltip.appendChild(questionMark);
          
          return tooltip;
        },
        
        /**
         * Creates a label element with optional tooltip
         * @param {string} text - Label text
         * @param {string} [forId=''] - ID of associated input
         * @param {string} [tooltip=''] - Optional tooltip text
         * @param {string} [className='setting-label'] - CSS class name
         * @returns {HTMLElement} Label element
         */
        createLabel(text, forId = '', tooltip = '', className = 'setting-label') {
          const label = document.createElement('label');
          label.className = className;
          if (forId) label.setAttribute('for', forId);
          
          label.textContent = text;
          
          if (tooltip) {
            label.appendChild(document.createTextNode(' '));
            label.appendChild(this.createTooltip(tooltip));
          }
          
          return label;
        },
        
        /**
         * Creates an inline help/description text element
         * @param {string} text - Help text
         * @returns {HTMLElement} Description span element
         */
        createDescription(text) {
          const help = document.createElement('span');
          help.className = 'setting-description';
          help.textContent = text;
          return help;
        },
        
        /**
         * Creates a range slider input
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {number} config.min - Minimum value
         * @param {number} config.max - Maximum value
         * @param {number} config.step - Step increment
         * @param {number} config.value - Initial value
         * @param {string} [config.label=''] - Optional label text
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @returns {HTMLElement} Container with slider (or just slider if no label)
         */
        createSlider(config) {
          const {
            id,
            min,
            max,
            step,
            value,
            label = '',
            tooltip = ''
          } = config;
          
          const slider = document.createElement('input');
          slider.type = 'range';
          slider.id = id;
          slider.min = min;
          slider.max = max;
          slider.step = step;
          slider.value = value;
          
          if (!label) return slider;
          
          const container = this.createSettingGroup();
          container.appendChild(this.createLabel(label, id, tooltip));
          container.appendChild(slider);
          
          return container;
        },
        
        /**
         * Creates a slider with synchronized value display
         * Automatically updates value display when slider moves
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {string} config.label - Label text
         * @param {number} config.min - Minimum value
         * @param {number} config.max - Maximum value
         * @param {number} config.step - Step increment
         * @param {number} config.value - Initial value
         * @param {string} [config.unit=''] - Unit to display (e.g., '%', 'px')
         * @param {string} [config.tooltip=''] - Optional tooltip text
         * @returns {HTMLElement} Container with label, slider, and value display
         */
        createSliderWithValue(config) {
          const {
            id,
            label,
            min,
            max,
            step,
            value,
            unit = '',
            tooltip = ''
          } = config;
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, id, tooltip));
          
          const sliderContainer = document.createElement('div');
          sliderContainer.className = 'slider-with-value';
          
          const slider = document.createElement('input');
          slider.type = 'range';
          slider.id = id;
          slider.min = min;
          slider.max = max;
          slider.step = step;
          slider.value = value;
          
          const valueDisplay = document.createElement('span');
          valueDisplay.className = 'value-display';
          
          const valueSpan = document.createElement('span');
          valueSpan.id = `${id}-value`;
          valueSpan.textContent = value;
          valueDisplay.appendChild(valueSpan);
          
          if (unit) {
            valueDisplay.appendChild(document.createTextNode(unit));
          }
          
          // Auto-update value display when slider moves
          slider.addEventListener('input', (e) => {
            valueSpan.textContent = e.target.value;
          });
          
          sliderContainer.appendChild(slider);
          sliderContainer.appendChild(valueDisplay);
          group.appendChild(sliderContainer);
          
          return group;
        },
        
        /**
         * Creates a text input field
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {string} config.label - Label text
         * @param {string} [config.value=''] - Initial value
         * @param {string} [config.placeholder=''] - Placeholder text
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @returns {HTMLElement} Container with label and input
         */
        createTextInput(config) {
          const {
            id,
            label,
            value = '',
            placeholder = '',
            tooltip = ''
          } = config;
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, id, tooltip));
          
          const input = document.createElement('input');
          input.type = 'text';
          input.id = id;
          input.value = value;
          if (placeholder) input.placeholder = placeholder;
          
          group.appendChild(input);
          return group;
        },
        
        /**
         * Creates a number input field
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {string} config.label - Label text
         * @param {number|string} [config.value=''] - Initial value
         * @param {number} [config.min] - Minimum value
         * @param {number} [config.max] - Maximum value
         * @param {number} [config.step=1] - Step increment
         * @param {string} [config.placeholder=''] - Placeholder text
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @returns {HTMLElement} Container with label and input
         */
        createNumberInput(config) {
          const {
            id,
            label,
            value = '',
            min,
            max,
            step = 1,
            placeholder = '',
            tooltip = ''
          } = config;
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, id, tooltip));
          
          const input = document.createElement('input');
          input.type = 'number';
          input.id = id;
          if (value !== '' && value !== null && value !== undefined) {
            input.value = value;
          }
          input.step = step;
          if (min !== undefined) input.min = min;
          if (max !== undefined) input.max = max;
          if (placeholder) input.placeholder = placeholder;
          
          group.appendChild(input);
          return group;
        },
        
        /**
         * Creates a textarea input field
         * @param {Object} config - Configuration object
         * @param {string} config.id - Textarea ID
         * @param {string} config.label - Label text
         * @param {string} [config.value=''] - Initial value
         * @param {string} [config.placeholder=''] - Placeholder text
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @param {string} [config.description=''] - Optional description text below label
         * @param {string} [config.rows='4'] - Number of visible rows
         * @param {string} [config.minHeight='100px'] - Minimum height
         * @returns {HTMLElement} Container with label, optional description, and textarea
         */
        createTextarea(config) {
          const {
            id,
            label,
            value = '',
            placeholder = '',
            tooltip = '',
            description = '',
            rows = '4',
            minHeight = '100px'
          } = config;
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, id, tooltip));
          
          // Add description if provided
          if (description) {
            group.appendChild(this.createDescription(description));
          }
          
          const textarea = document.createElement('textarea');
          textarea.id = id;
          textarea.value = value;
          textarea.rows = rows;
          textarea.style.minHeight = minHeight;
          textarea.style.resize = 'vertical';
          if (placeholder) textarea.placeholder = placeholder;
          
          group.appendChild(textarea);
          return group;
        },
        
        /**
         * Creates a checkbox input
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {string} config.label - Label text
         * @param {boolean} [config.checked=false] - Initial checked state
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @param {boolean} [config.inGroup=true] - Wrap in setting-group div
         * @returns {HTMLElement} Label element (or container if inGroup=true)
         */
        createCheckbox(config) {
          const {
            id,
            label,
            checked = false,
            tooltip = '',
            inGroup = true
          } = config;
          
          const checkbox = document.createElement('input');
          checkbox.type = 'checkbox';
          checkbox.id = id;
          checkbox.checked = checked;
          
          const labelElement = document.createElement('label');
          labelElement.className = 'checkbox-label';
          labelElement.appendChild(checkbox);
          labelElement.appendChild(document.createTextNode(' ' + label));
          
          if (tooltip) {
            labelElement.appendChild(document.createTextNode(' '));
            labelElement.appendChild(this.createTooltip(tooltip));
          }
          
          if (!inGroup) return labelElement;
          
          const group = this.createSettingGroup();
          group.appendChild(labelElement);
          return group;
        },
        
        /**
         * Creates a checkbox with conditional subsettings that show/hide
         * Common pattern: checkbox that reveals additional options when checked
         * @param {Object} config - Configuration object
         * @param {string} config.id - Checkbox ID
         * @param {string} config.label - Checkbox label
         * @param {boolean} [config.checked=false] - Initial checked state
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @param {HTMLElement|Array<HTMLElement>} config.subsettings - Elements to show/hide
         * @returns {HTMLElement} Container with checkbox and conditional subsettings
         */
        createConditionalCheckbox(config) {
          const {
            id,
            label,
            checked = false,
            tooltip = '',
            subsettings
          } = config;
          
          const container = this.createSettingGroup();
          
          // Create checkbox
          const checkboxLabel = this.createCheckbox({
            id,
            label,
            checked,
            tooltip,
            inGroup: false
          });
          container.appendChild(checkboxLabel);
          
          // Create subsettings container
          const subsettingsContainer = this.createSubsettings();
          subsettingsContainer.style.display = checked ? '' : 'none';
          
          // Add subsettings content
          if (Array.isArray(subsettings)) {
            subsettings.forEach(element => {
              if (element instanceof HTMLElement) {
                subsettingsContainer.appendChild(element);
              }
            });
          } else if (subsettings instanceof HTMLElement) {
            subsettingsContainer.appendChild(subsettings);
          }
          
          container.appendChild(subsettingsContainer);
          
          // Auto-toggle visibility using getElementById (more robust than querySelector)
          const checkbox = document.getElementById(id);
          if (checkbox) {
            checkbox.addEventListener('change', (e) => {
              subsettingsContainer.style.display = e.target.checked ? '' : 'none';
            });
          }
          
          return container;
        },
        
        /**
         * Creates a radio button group
         * @param {Object} config - Configuration object
         * @param {string} config.name - Radio group name (all radios share this)
         * @param {string} config.label - Group label text
         * @param {Array<{value: string, label: string, checked?: boolean}>} config.options - Radio options
         * @param {string} [config.tooltip=''] - Optional tooltip for group label
         * @returns {HTMLElement} Container with label and radio buttons
         */
        createRadioGroup(config) {
          const {
            name,
            label,
            options,
            tooltip = ''
          } = config;
          
          if (!options || !Array.isArray(options)) {
            console.error('[AO3 Menu Helpers] createRadioGroup: options must be an array');
            return this.createSettingGroup();
          }
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, '', tooltip));
          
          options.forEach(option => {
            const radio = document.createElement('input');
            radio.type = 'radio';
            radio.name = name;
            radio.value = option.value;
            radio.id = `${name}-${option.value}`;
            if (option.checked) radio.checked = true;
            
            const radioLabel = document.createElement('label');
            radioLabel.className = 'radio-label';
            radioLabel.appendChild(radio);
            radioLabel.appendChild(document.createTextNode(' ' + option.label));
            
            group.appendChild(radioLabel);
          });
          
          return group;
        },
        
        /**
         * Creates a select dropdown
         * @param {Object} config - Configuration object
         * @param {string} config.id - Select ID
         * @param {string} config.label - Label text
         * @param {Array<{value: string, label: string, selected?: boolean}>} config.options - Select options
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @returns {HTMLElement} Container with label and select
         */
        createSelect(config) {
          const {
            id,
            label,
            options,
            tooltip = ''
          } = config;
          
          if (!options || !Array.isArray(options)) {
            console.error('[AO3 Menu Helpers] createSelect: options must be an array');
            return this.createSettingGroup();
          }
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, id, tooltip));
          
          const select = document.createElement('select');
          select.id = id;
          
          options.forEach(option => {
            const optionElement = document.createElement('option');
            optionElement.value = option.value;
            optionElement.textContent = option.label;
            if (option.selected) optionElement.selected = true;
            select.appendChild(optionElement);
          });
          
          group.appendChild(select);
          return group;
        },
        
        /**
         * Creates a color picker input
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {string} config.label - Label text
         * @param {string} [config.value='#000000'] - Initial color value
         * @param {string} [config.tooltip=''] - Optional tooltip
         * @returns {HTMLElement} Container with label and color input
         */
        createColorPicker(config) {
          const {
            id,
            label,
            value = '#000000',
            tooltip = ''
          } = config;
          
          const group = this.createSettingGroup();
          group.appendChild(this.createLabel(label, id, tooltip));
          
          const input = document.createElement('input');
          input.type = 'color';
          input.id = id;
          input.value = value;
          
          group.appendChild(input);
          return group;
        },
        
        /**
         * Creates a two-column layout
         * @param {HTMLElement} leftContent - Left column content
         * @param {HTMLElement} rightContent - Right column content
         * @returns {HTMLElement} Two-column container
         */
        createTwoColumnLayout(leftContent, rightContent) {
          const container = document.createElement('div');
          container.className = 'two-column';
          
          if (leftContent instanceof HTMLElement) {
            container.appendChild(leftContent);
          }
          if (rightContent instanceof HTMLElement) {
            container.appendChild(rightContent);
          }
          
          return container;
        },
        
        /**
         * Creates a subsettings container (indented settings)
         * @param {HTMLElement|string} [content=''] - Content to place inside
         * @returns {HTMLElement} Subsettings div
         */
        createSubsettings(content = '') {
          const subsettings = document.createElement('div');
          subsettings.className = 'subsettings';
          
          if (typeof content === 'string' && content) {
            subsettings.innerHTML = content;
          } else if (content instanceof HTMLElement) {
            subsettings.appendChild(content);
          }
          
          return subsettings;
        },
        
        /**
         * Creates a button group (typically for Save/Cancel)
         * @param {Array<{text: string, id: string, primary?: boolean, onClick?: function}>} buttons - Button configurations
         * @returns {HTMLElement} Button group container
         */
        createButtonGroup(buttons) {
          if (!buttons || !Array.isArray(buttons)) {
            console.error('[AO3 Menu Helpers] createButtonGroup: buttons must be an array');
            return document.createElement('div');
          }
          
          const group = document.createElement('div');
          group.className = 'button-group';
          
          buttons.forEach(btnConfig => {
            const button = document.createElement('button');
            button.type = 'button';
            button.textContent = btnConfig.text;
            if (btnConfig.id) button.id = btnConfig.id;
            if (btnConfig.primary) button.classList.add('primary');
            if (btnConfig.onClick) button.addEventListener('click', btnConfig.onClick);
            
            group.appendChild(button);
          });
          
          return group;
        },
        
        /**
         * Creates a reset link
         * @param {string} text - Link text
         * @param {function} onResetCallback - Function to call when clicked
         * @returns {HTMLElement} Reset link container
         */
        createResetLink(text, onResetCallback) {
          const container = document.createElement('div');
          container.className = 'reset-link';
          
          const link = document.createElement('a');
          link.href = '#';
          link.textContent = text;
          link.addEventListener('click', (e) => {
            e.preventDefault();
            if (typeof onResetCallback === 'function') {
              onResetCallback();
            }
          });
          
          container.appendChild(link);
          return container;
        },
        
        /**
         * Creates a keyboard key visual element
         * @param {string} keyText - Text to display (e.g., 'Alt', 'Ctrl')
         * @returns {HTMLElement} Styled kbd element
         */
        createKeyboardKey(keyText) {
          const kbd = document.createElement('kbd');
          kbd.textContent = keyText;
          return kbd;
        },
        
        /**
         * Creates an info/tip box with border and background
         * @param {string|HTMLElement} content - HTML content, text, or element
         * @param {Object} [options={}] - Optional styling
         * @param {string} [options.icon='💡'] - Icon to display
         * @param {string} [options.title=''] - Optional title
         * @returns {HTMLElement} Styled info box
         */
        createInfoBox(content, options = {}) {
          const {
            icon = '💡',
            title = ''
          } = options;
          
          const box = document.createElement('div');
          box.style.cssText = `
            padding: 12px;
            margin: 15px 0;
            background: rgba(0,0,0,0.03);
            border-radius: 6px;
            border-left: 4px solid currentColor;
          `;
          
          const p = document.createElement('p');
          p.style.cssText = 'margin: 0; font-size: 0.9em; opacity: 0.8;';
          
          let html = '';
          if (title) {
            html += `<strong>${icon} ${title}:</strong> `;
          } else if (icon) {
            html += `${icon} `;
          }
          
          if (typeof content === 'string') {
            p.innerHTML = html + content;
          } else if (content instanceof HTMLElement) {
            if (html) {
              const span = document.createElement('span');
              span.innerHTML = html;
              p.appendChild(span);
            }
            p.appendChild(content);
          } else {
            console.warn('[AO3 Menu Helpers] Invalid content type for createInfoBox');
            p.innerHTML = html + String(content);
          }
          
          box.appendChild(p);
          return box;
        },
        
        /**
         * Creates a file input button with custom styling
         * @param {Object} config - Configuration object
         * @param {string} config.id - Input ID
         * @param {string} config.buttonText - Button text
         * @param {string} [config.accept=''] - File accept attribute
         * @param {function} [config.onChange] - Change event handler (receives file as parameter)
         * @returns {Object} Object with {button, input} elements
         */
        createFileInput(config) {
          const {
            id,
            buttonText,
            accept = '',
            onChange
          } = config;
          
          const input = document.createElement('input');
          input.type = 'file';
          input.id = id;
          input.style.display = 'none';
          if (accept) input.accept = accept;
          
          const button = document.createElement('button');
          button.type = 'button';
          button.textContent = buttonText;
          button.addEventListener('click', () => {
            input.value = '';
            input.click();
          });
          
          if (onChange) {
            input.addEventListener('change', (e) => {
              const file = e.target.files && e.target.files[0];
              if (file) onChange(file);
            });
          }
          
          return { button, input };
        },
        
        /**
         * Creates a horizontal layout container
         * @param {Array<HTMLElement>} elements - Elements to place horizontally
         * @param {Object} [options={}] - Layout options
         * @param {string} [options.gap='8px'] - Gap between elements
         * @param {string} [options.justifyContent='flex-start'] - Flex justify-content
         * @param {string} [options.alignItems='center'] - Flex align-items
         * @returns {HTMLElement} Horizontal layout container
         */
        createHorizontalLayout(elements, options = {}) {
          const {
            gap = '8px',
            justifyContent = 'flex-start',
            alignItems = 'center'
          } = options;
          
          const container = document.createElement('div');
          container.style.cssText = `
            display: flex;
            gap: ${gap};
            justify-content: ${justifyContent};
            align-items: ${alignItems};
            flex-wrap: wrap;
          `;
          
          if (Array.isArray(elements)) {
            elements.forEach(el => {
              if (el instanceof HTMLElement) {
                container.appendChild(el);
              }
            });
          }
          
          return container;
        },
        
        /**
         * Removes all dialogs with .ao3-menu-dialog class from the page
         */
        removeAllDialogs() {
          document.querySelectorAll('.ao3-menu-dialog').forEach(dialog => {
            dialog.remove();
          });
        },
        
        /**
         * Helper to get value from an input by ID
         * Returns appropriate type based on input type
         * @param {string} id - Input element ID
         * @returns {string|number|boolean|null} Input value or null if not found
         */
        getValue(id) {
          const element = document.getElementById(id);
          if (!element) return null;
          
          if (element.type === 'checkbox') {
            return element.checked;
          } else if (element.type === 'number' || element.type === 'range') {
            const val = parseFloat(element.value);
            return isNaN(val) ? null : val;
          } else if (element.type === 'radio') {
            const name = element.name || '';
            // Use getElementById with checked property instead of querySelector for safety
            const radios = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
            for (const radio of radios) {
              if (radio.checked) return radio.value;
            }
            return null;
          }
          
          return element.value;
        },
        
        /**
         * Helper to set value of an input by ID
         * Handles different input types appropriately
         * @param {string} id - Input element ID
         * @param {*} value - Value to set
         * @returns {boolean} True if successful, false otherwise
         */
        setValue(id, value) {
          const element = document.getElementById(id);
          if (!element) return false;
          
          if (element.type === 'checkbox') {
            element.checked = Boolean(value);
          } else if (element.type === 'radio') {
            const radio = document.querySelector(`input[name="${element.name}"][value="${value}"]`);
            if (radio) radio.checked = true;
          } else {
            element.value = value;
          }
          
          // Trigger change/input events
          element.dispatchEvent(new Event('input', { bubbles: true }));
          element.dispatchEvent(new Event('change', { bubbles: true }));
          
          return true;
        }
      };
      
      // Auto-inject styles when library loads (if document.head is ready)
      if (document.head) {
        window.AO3MenuHelpers.injectSharedStyles();
      } else {
        // Wait for head to be available
        const observer = new MutationObserver(() => {
          if (document.head) {
            observer.disconnect();
            window.AO3MenuHelpers.injectSharedStyles();
          }
        });
        observer.observe(document.documentElement, { childList: true });
      }
      
      console.log('[AO3 Menu Helpers] Library loaded, version', window.AO3MenuHelpers.version);
    })();