Greasy Fork is available in English.
Shared UI components and styling for AO3 userscripts
当前为 
        此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/552743/1678303/AO3%3A%20Menu%20Helpers%20Library.js
      
// ==UserScript==
// @name         AO3: Menu Helpers Library
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @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.0',
    
    /**
     * 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;
    }
  };
  
  console.log('[AO3 Menu Helpers] Library loaded, version', window.AO3MenuHelpers.version);
})();