Chapter 11 User Interface Design and Experience Optimization

Haiyue
58min

Chapter 11: User Interface Design and Experience Optimization

Learning Objectives

  1. Master Chrome Extension UI design best practices
  2. Learn to implement responsive and adaptive interfaces
  3. Optimize user interaction experience and performance
  4. Implement accessibility and internationalization support

11.1 UI Design Principles

11.1.1 Design Philosophy

🔄 正在渲染 Mermaid 图表...

11.1.2 Modern UI Framework Integration

// ui-framework.js - Lightweight UI framework
class ChromeExtensionUI {
  constructor() {
    this.components = new Map();
    this.themes = new Map();
    this.currentTheme = 'light';
    this.animations = new Map();
    this.init();
  }

  init() {
    this.setupThemes();
    this.registerBaseComponents();
    this.setupGlobalStyles();
    this.bindEvents();
  }

  setupThemes() {
    // Light theme
    this.themes.set('light', {
      primary: '#4285f4',
      secondary: '#34a853',
      danger: '#ea4335',
      warning: '#fbbc05',
      background: '#ffffff',
      surface: '#f8f9fa',
      'on-background': '#202124',
      'on-surface': '#5f6368',
      border: '#dadce0',
      shadow: '0 2px 4px rgba(0,0,0,0.1)'
    });

    // Dark theme
    this.themes.set('dark', {
      primary: '#8ab4f8',
      secondary: '#81c995',
      danger: '#f28b82',
      warning: '#fdd663',
      background: '#202124',
      surface: '#303134',
      'on-background': '#e8eaed',
      'on-surface': '#9aa0a6',
      border: '#5f6368',
      shadow: '0 2px 4px rgba(0,0,0,0.3)'
    });
  }

  registerBaseComponents() {
    // Button component
    this.registerComponent('button', {
      template: `
        <button class="ce-button {{variant}} {{size}} {{disabled}}"
                {{disabled}} data-ripple="true">
          {{#if icon}}<span class="ce-button-icon">{{icon}}</span>{{/if}}
          <span class="ce-button-text">{{text}}</span>
        </button>
      `,
      props: {
        text: '',
        icon: '',
        variant: 'primary', // primary, secondary, outline, ghost
        size: 'medium', // small, medium, large
        disabled: false,
        onClick: null
      },
      mounted() {
        this.addRippleEffect();
        if (this.props.onClick) {
          this.element.addEventListener('click', this.props.onClick);
        }
      }
    });

    // Input component
    this.registerComponent('input', {
      template: `
        <div class="ce-input-group {{state}}">
          {{#if label}}<label class="ce-input-label">{{label}}</label>{{/if}}
          <input type="{{type}}" class="ce-input" placeholder="{{placeholder}}"
                 value="{{value}}" {{required}} {{disabled}}>
          {{#if helperText}}<span class="ce-input-helper">{{helperText}}</span>{{/if}}
          {{#if errorText}}<span class="ce-input-error">{{errorText}}</span>{{/if}}
        </div>
      `,
      props: {
        type: 'text',
        label: '',
        placeholder: '',
        value: '',
        helperText: '',
        errorText: '',
        required: false,
        disabled: false,
        state: 'normal' // normal, error, success
      },
      mounted() {
        this.setupValidation();
        this.setupFloatingLabel();
      }
    });

    // Card component
    this.registerComponent('card', {
      template: `
        <div class="ce-card {{variant}} {{elevation}}">
          {{#if header}}
          <div class="ce-card-header">
            {{#if title}}<h3 class="ce-card-title">{{title}}</h3>{{/if}}
            {{#if subtitle}}<p class="ce-card-subtitle">{{subtitle}}</p>{{/if}}
            {{#if actions}}<div class="ce-card-actions">{{actions}}</div>{{/if}}
          </div>
          {{/if}}
          <div class="ce-card-content">{{content}}</div>
          {{#if footer}}
          <div class="ce-card-footer">{{footer}}</div>
          {{/if}}
        </div>
      `,
      props: {
        title: '',
        subtitle: '',
        content: '',
        actions: '',
        footer: '',
        variant: 'default', // default, outlined, filled
        elevation: 'medium' // none, low, medium, high
      }
    });

    // Modal component
    this.registerComponent('modal', {
      template: `
        <div class="ce-modal-overlay {{visible}}" role="dialog" aria-modal="true">
          <div class="ce-modal-container {{size}}">
            <div class="ce-modal-header">
              <h2 class="ce-modal-title">{{title}}</h2>
              <button class="ce-modal-close" aria-label="Close">&times;</button>
            </div>
            <div class="ce-modal-content">{{content}}</div>
            <div class="ce-modal-footer">{{footer}}</div>
          </div>
        </div>
      `,
      props: {
        title: '',
        content: '',
        footer: '',
        size: 'medium', // small, medium, large
        visible: false,
        onClose: null
      },
      mounted() {
        this.setupCloseHandlers();
        this.setupKeyboardNavigation();
        this.setupFocusTrap();
      }
    });
  }

  registerComponent(name, definition) {
    this.components.set(name, definition);
  }

  createElement(componentName, props = {}) {
    const component = this.components.get(componentName);
    if (!component) {
      throw new Error(`Component ${componentName} does not exist`);
    }

    const mergedProps = { ...component.props, ...props };
    const html = this.renderTemplate(component.template, mergedProps);

    const element = this.createElementFromHTML(html);

    // Create component instance
    const instance = {
      element,
      props: mergedProps,
      component,
      // Add component methods
      addRippleEffect: this.addRippleEffect.bind({ element }),
      setupValidation: this.setupValidation.bind({ element }),
      setupFloatingLabel: this.setupFloatingLabel.bind({ element }),
      setupCloseHandlers: this.setupCloseHandlers.bind({ element, props: mergedProps }),
      setupKeyboardNavigation: this.setupKeyboardNavigation.bind({ element }),
      setupFocusTrap: this.setupFocusTrap.bind({ element })
    };

    // Call mounted lifecycle
    if (component.mounted) {
      component.mounted.call(instance);
    }

    return instance;
  }

  renderTemplate(template, props) {
    return template.replace(/\{\{(\#if\s+)?(\#unless\s+)?([^}]+)\}\}/g, (match, ifStatement, unlessStatement, propPath) => {
      if (ifStatement) {
        const prop = propPath.trim();
        return props[prop] ? '' : '<!--';
      }

      if (unlessStatement) {
        const prop = propPath.trim();
        return !props[prop] ? '' : '<!--';
      }

      const value = this.getNestedProp(props, propPath.trim());
      return value !== undefined ? value : '';
    });
  }

  getNestedProp(obj, path) {
    return path.split('.').reduce((current, key) => current?.[key], obj);
  }

  createElementFromHTML(html) {
    const template = document.createElement('template');
    template.innerHTML = html.trim();
    return template.content.firstChild;
  }

  // Component method implementations
  addRippleEffect() {
    this.element.addEventListener('click', (e) => {
      const ripple = document.createElement('span');
      const rect = this.element.getBoundingClientRect();
      const size = Math.max(rect.width, rect.height);
      const x = e.clientX - rect.left - size / 2;
      const y = e.clientY - rect.top - size / 2;

      ripple.style.cssText = `
        position: absolute;
        border-radius: 50%;
        background: rgba(255, 255, 255, 0.6);
        transform: scale(0);
        animation: ripple 0.6s linear;
        left: ${x}px;
        top: ${y}px;
        width: ${size}px;
        height: ${size}px;
      `;

      this.element.appendChild(ripple);

      setTimeout(() => {
        ripple.remove();
      }, 600);
    });
  }

  setupValidation() {
    const input = this.element.querySelector('.ce-input');
    if (!input) return;

    input.addEventListener('blur', () => {
      this.validateInput(input);
    });

    input.addEventListener('input', () => {
      if (this.element.classList.contains('error')) {
        this.validateInput(input);
      }
    });
  }

  validateInput(input) {
    const value = input.value.trim();
    const required = input.hasAttribute('required');

    let isValid = true;
    let errorMessage = '';

    if (required && !value) {
      isValid = false;
      errorMessage = 'This field is required';
    } else if (input.type === 'email' && value && !this.isValidEmail(value)) {
      isValid = false;
      errorMessage = 'Please enter a valid email address';
    } else if (input.type === 'url' && value && !this.isValidUrl(value)) {
      isValid = false;
      errorMessage = 'Please enter a valid URL';
    }

    this.element.classList.toggle('error', !isValid);
    this.element.classList.toggle('success', isValid && value);

    const errorElement = this.element.querySelector('.ce-input-error');
    if (errorElement) {
      errorElement.textContent = errorMessage;
    }

    return isValid;
  }

  isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  isValidUrl(url) {
    try {
      new URL(url);
      return true;
    } catch {
      return false;
    }
  }

  setupFloatingLabel() {
    const input = this.element.querySelector('.ce-input');
    const label = this.element.querySelector('.ce-input-label');

    if (!input || !label) return;

    const updateLabelState = () => {
      const hasValue = input.value.trim() !== '';
      const isFocused = document.activeElement === input;

      this.element.classList.toggle('focused', isFocused);
      this.element.classList.toggle('has-value', hasValue);
    };

    input.addEventListener('focus', updateLabelState);
    input.addEventListener('blur', updateLabelState);
    input.addEventListener('input', updateLabelState);

    // Initial state
    updateLabelState();
  }

  setupCloseHandlers() {
    const closeBtn = this.element.querySelector('.ce-modal-close');
    const overlay = this.element.querySelector('.ce-modal-overlay');

    if (closeBtn && this.props.onClose) {
      closeBtn.addEventListener('click', this.props.onClose);
    }

    if (overlay && this.props.onClose) {
      overlay.addEventListener('click', (e) => {
        if (e.target === overlay) {
          this.props.onClose();
        }
      });
    }
  }

  setupKeyboardNavigation() {
    this.element.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && this.props.onClose) {
        this.props.onClose();
      }
    });
  }

  setupFocusTrap() {
    const focusableElements = this.element.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    const firstFocusable = focusableElements[0];
    const lastFocusable = focusableElements[focusableElements.length - 1];

    this.element.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        if (e.shiftKey) {
          if (document.activeElement === firstFocusable) {
            e.preventDefault();
            lastFocusable.focus();
          }
        } else {
          if (document.activeElement === lastFocusable) {
            e.preventDefault();
            firstFocusable.focus();
          }
        }
      }
    });

    // Auto-focus first element
    firstFocusable?.focus();
  }

  // Theme management
  setTheme(themeName) {
    const theme = this.themes.get(themeName);
    if (!theme) return;

    this.currentTheme = themeName;
    const root = document.documentElement;

    Object.entries(theme).forEach(([property, value]) => {
      root.style.setProperty(`--ce-${property}`, value);
    });

    document.body.setAttribute('data-theme', themeName);
  }

  getCurrentTheme() {
    return this.currentTheme;
  }

  // Animation management
  registerAnimation(name, keyframes, options = {}) {
    this.animations.set(name, { keyframes, options });
  }

  animate(element, animationName, customOptions = {}) {
    const animation = this.animations.get(animationName);
    if (!animation) return;

    const options = { ...animation.options, ...customOptions };
    return element.animate(animation.keyframes, options);
  }

  setupGlobalStyles() {
    const styles = `
      .ce-button {
        position: relative;
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 8px 16px;
        border: none;
        border-radius: 4px;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s ease;
        overflow: hidden;
      }

      .ce-button.primary {
        background-color: var(--ce-primary);
        color: white;
      }

      .ce-button.secondary {
        background-color: var(--ce-secondary);
        color: white;
      }

      .ce-button.outline {
        background-color: transparent;
        color: var(--ce-primary);
        border: 1px solid var(--ce-primary);
      }

      .ce-button.ghost {
        background-color: transparent;
        color: var(--ce-primary);
      }

      .ce-button:hover:not([disabled]) {
        transform: translateY(-1px);
        box-shadow: var(--ce-shadow);
      }

      .ce-button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
        transform: none;
      }

      .ce-input-group {
        position: relative;
        margin-bottom: 16px;
      }

      .ce-input {
        width: 100%;
        padding: 12px;
        border: 1px solid var(--ce-border);
        border-radius: 4px;
        font-size: 14px;
        transition: all 0.2s ease;
      }

      .ce-input:focus {
        outline: none;
        border-color: var(--ce-primary);
        box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
      }

      .ce-input-label {
        display: block;
        margin-bottom: 4px;
        font-size: 12px;
        font-weight: 500;
        color: var(--ce-on-surface);
      }

      .ce-input-helper {
        display: block;
        margin-top: 4px;
        font-size: 12px;
        color: var(--ce-on-surface);
      }

      .ce-input-error {
        display: block;
        margin-top: 4px;
        font-size: 12px;
        color: var(--ce-danger);
      }

      .ce-input-group.error .ce-input {
        border-color: var(--ce-danger);
      }

      .ce-input-group.success .ce-input {
        border-color: var(--ce-secondary);
      }

      .ce-card {
        background-color: var(--ce-surface);
        border-radius: 8px;
        overflow: hidden;
        transition: all 0.2s ease;
      }

      .ce-card.outlined {
        border: 1px solid var(--ce-border);
      }

      .ce-card.elevation-low {
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      }

      .ce-card.elevation-medium {
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }

      .ce-card.elevation-high {
        box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
      }

      .ce-card-header {
        padding: 16px;
        border-bottom: 1px solid var(--ce-border);
      }

      .ce-card-title {
        margin: 0;
        font-size: 16px;
        font-weight: 500;
      }

      .ce-card-subtitle {
        margin: 4px 0 0 0;
        font-size: 14px;
        color: var(--ce-on-surface);
      }

      .ce-card-content {
        padding: 16px;
      }

      .ce-card-footer {
        padding: 16px;
        border-top: 1px solid var(--ce-border);
      }

      .ce-modal-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        display: flex;
        align-items: center;
        justify-content: center;
        z-index: 1000;
        opacity: 0;
        visibility: hidden;
        transition: all 0.3s ease;
      }

      .ce-modal-overlay.visible {
        opacity: 1;
        visibility: visible;
      }

      .ce-modal-container {
        background-color: var(--ce-background);
        border-radius: 8px;
        max-height: 90vh;
        overflow: hidden;
        transform: scale(0.9);
        transition: transform 0.3s ease;
      }

      .ce-modal-overlay.visible .ce-modal-container {
        transform: scale(1);
      }

      .ce-modal-container.small {
        width: 400px;
      }

      .ce-modal-container.medium {
        width: 600px;
      }

      .ce-modal-container.large {
        width: 800px;
      }

      .ce-modal-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 16px;
        border-bottom: 1px solid var(--ce-border);
      }

      .ce-modal-title {
        margin: 0;
        font-size: 18px;
        font-weight: 500;
      }

      .ce-modal-close {
        background: none;
        border: none;
        font-size: 24px;
        cursor: pointer;
        color: var(--ce-on-surface);
      }

      .ce-modal-content {
        padding: 16px;
        max-height: 60vh;
        overflow-y: auto;
      }

      .ce-modal-footer {
        padding: 16px;
        border-top: 1px solid var(--ce-border);
        display: flex;
        gap: 8px;
        justify-content: flex-end;
      }

      @keyframes ripple {
        to {
          transform: scale(4);
          opacity: 0;
        }
      }

      @media (max-width: 480px) {
        .ce-modal-container {
          width: 90vw !important;
          max-height: 80vh;
        }
      }
    `;

    const styleElement = document.createElement('style');
    styleElement.textContent = styles;
    document.head.appendChild(styleElement);
  }

  bindEvents() {
    // Global keyboard events
    document.addEventListener('keydown', (e) => {
      // Escape key closes modals
      if (e.key === 'Escape') {
        const visibleModal = document.querySelector('.ce-modal-overlay.visible');
        if (visibleModal) {
          const closeBtn = visibleModal.querySelector('.ce-modal-close');
          closeBtn?.click();
        }
      }
    });
  }
}

// Usage example
const ui = new ChromeExtensionUI();

// Create button
const saveButton = ui.createElement('button', {
  text: 'Save',
  icon: '💾',
  variant: 'primary',
  onClick: () => console.log('Save button clicked')
});

// Create input
const emailInput = ui.createElement('input', {
  type: 'email',
  label: 'Email Address',
  placeholder: 'Please enter email',
  required: true
});

// Create card
const infoCard = ui.createElement('card', {
  title: 'Extension Info',
  subtitle: 'Version 1.0.0',
  content: '<p>This is a powerful Chrome extension.</p>'
});

// Export UI framework
window.ChromeExtensionUI = ChromeExtensionUI;

11.2 Responsive Design

11.2.1 Adaptive Layout System

/* responsive-layout.css - Responsive layout system */
:root {
  /* Breakpoint definitions */
  --mobile: 480px;
  --tablet: 768px;
  --desktop: 1024px;
  --large: 1200px;

  /* Spacing system */
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;

  /* Typography system */
  --font-xs: 12px;
  --font-sm: 14px;
  --font-md: 16px;
  --font-lg: 18px;
  --font-xl: 20px;
  --font-xxl: 24px;
}

/* Container system */
.container {
  width: 100%;
  max-width: var(--large);
  margin: 0 auto;
  padding: 0 var(--space-md);
}

.container-fluid {
  width: 100%;
  padding: 0 var(--space-md);
}

/* Grid system */
.grid {
  display: grid;
  gap: var(--space-md);
}

.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
.grid-cols-6 { grid-template-columns: repeat(6, 1fr); }
.grid-cols-12 { grid-template-columns: repeat(12, 1fr); }

/* Flexbox layout */
.flex {
  display: flex;
}

.flex-col {
  flex-direction: column;
}

.flex-wrap {
  flex-wrap: wrap;
}

.items-center {
  align-items: center;
}

.items-start {
  align-items: flex-start;
}

.items-end {
  align-items: flex-end;
}

.justify-center {
  justify-content: center;
}

.justify-between {
  justify-content: space-between;
}

.justify-around {
  justify-content: space-around;
}

.flex-1 {
  flex: 1;
}

/* Spacing utility classes */
.p-0 { padding: 0; }
.p-xs { padding: var(--space-xs); }
.p-sm { padding: var(--space-sm); }
.p-md { padding: var(--space-md); }
.p-lg { padding: var(--space-lg); }
.p-xl { padding: var(--space-xl); }

.m-0 { margin: 0; }
.m-xs { margin: var(--space-xs); }
.m-sm { margin: var(--space-sm); }
.m-md { margin: var(--space-md); }
.m-lg { margin: var(--space-lg); }
.m-xl { margin: var(--space-xl); }

.mt-auto { margin-top: auto; }
.mb-auto { margin-bottom: auto; }
.ml-auto { margin-left: auto; }
.mr-auto { margin-right: auto; }

/* Text styles */
.text-xs { font-size: var(--font-xs); }
.text-sm { font-size: var(--font-sm); }
.text-md { font-size: var(--font-md); }
.text-lg { font-size: var(--font-lg); }
.text-xl { font-size: var(--font-xl); }

.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-bold { font-weight: 700; }

.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }

/* Display controls */
.hidden { display: none; }
.block { display: block; }
.inline { display: inline; }
.inline-block { display: inline-block; }

/* Responsive breakpoints */
@media (max-width: 480px) {
  .mobile\:hidden { display: none; }
  .mobile\:block { display: block; }
  .mobile\:flex { display: flex; }
  .mobile\:grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
  .mobile\:flex-col { flex-direction: column; }
  .mobile\:text-center { text-align: center; }

  .container {
    padding: 0 var(--space-sm);
  }

  .grid {
    gap: var(--space-sm);
  }
}

@media (min-width: 481px) and (max-width: 768px) {
  .tablet\:hidden { display: none; }
  .tablet\:block { display: block; }
  .tablet\:flex { display: flex; }
  .tablet\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
  .tablet\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 769px) {
  .desktop\:hidden { display: none; }
  .desktop\:block { display: block; }
  .desktop\:flex { display: flex; }
  .desktop\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
  .desktop\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
}

/* Extension-specific responsive classes */
.extension-popup {
  width: 400px;
  min-height: 300px;
  max-height: 600px;
}

@media (max-width: 480px) {
  .extension-popup {
    width: 320px;
    min-height: 250px;
  }
}

.extension-options {
  max-width: 800px;
  margin: 0 auto;
}

@media (max-width: 768px) {
  .extension-options {
    max-width: 100%;
    padding: var(--space-sm);
  }
}

/* Scrollable containers */
.scrollable {
  overflow-y: auto;
  max-height: 400px;
}

.scrollable::-webkit-scrollbar {
  width: 6px;
}

.scrollable::-webkit-scrollbar-track {
  background: var(--ce-surface);
}

.scrollable::-webkit-scrollbar-thumb {
  background: var(--ce-border);
  border-radius: 3px;
}

.scrollable::-webkit-scrollbar-thumb:hover {
  background: var(--ce-on-surface);
}

/* Loading state */
.loading {
  position: relative;
  pointer-events: none;
  opacity: 0.6;
}

.loading::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border: 2px solid var(--ce-border);
  border-top: 2px solid var(--ce-primary);
  border-radius: 50%;
  animation: spin 1s linear infinite;
  z-index: 1;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* Transition animations */
.transition {
  transition: all 0.2s ease;
}

.transition-fast {
  transition: all 0.1s ease;
}

.transition-slow {
  transition: all 0.3s ease;
}

/* Hover effects */
.hover-lift:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}

.hover-scale:hover {
  transform: scale(1.02);
}

/* Focus state */
.focus-visible {
  outline: 2px solid var(--ce-primary);
  outline-offset: 2px;
}

/* Error state */
.error {
  border-color: var(--ce-danger) !important;
  color: var(--ce-danger);
}

.success {
  border-color: var(--ce-secondary) !important;
  color: var(--ce-secondary);
}

/* Disabled state */
.disabled {
  opacity: 0.6;
  pointer-events: none;
  cursor: not-allowed;
}

/* Helper classes */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.line-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.line-clamp-3 {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* High contrast mode support */
@media (prefers-contrast: high) {
  :root {
    --ce-border: #000000;
    --ce-primary: #0000ff;
    --ce-danger: #ff0000;
    --ce-secondary: #008000;
  }
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Print styles */
@media print {
  .no-print {
    display: none !important;
  }

  .extension-popup,
  .extension-options {
    width: auto !important;
    max-width: none !important;
    max-height: none !important;
  }

  .scrollable {
    max-height: none !important;
    overflow: visible !important;
  }
}

11.3 Performance Optimization

11.3.1 Rendering Performance Optimization

// performance-optimizer.js - Performance optimization tools
class PerformanceOptimizer {
  constructor() {
    this.observers = new Map();
    this.debounceTimers = new Map();
    this.throttleTimers = new Map();
    this.virtualScrollers = new Map();
    this.imageLoaders = new Map();
    this.init();
  }

  init() {
    this.setupIntersectionObserver();
    this.setupPerformanceMonitoring();
    this.setupResourceOptimization();
  }

  // Debounce function
  debounce(func, wait, immediate = false) {
    const key = func.toString();

    return (...args) => {
      const later = () => {
        this.debounceTimers.delete(key);
        if (!immediate) func.apply(this, args);
      };

      const callNow = immediate && !this.debounceTimers.has(key);

      if (this.debounceTimers.has(key)) {
        clearTimeout(this.debounceTimers.get(key));
      }

      this.debounceTimers.set(key, setTimeout(later, wait));

      if (callNow) func.apply(this, args);
    };
  }

  // Throttle function
  throttle(func, wait) {
    const key = func.toString();

    return (...args) => {
      if (this.throttleTimers.has(key)) {
        return;
      }

      this.throttleTimers.set(key, setTimeout(() => {
        this.throttleTimers.delete(key);
      }, wait));

      func.apply(this, args);
    };
  }

  // Virtual scrolling implementation
  createVirtualScroller(container, items, renderItem, itemHeight = 50) {
    const scroller = {
      container,
      items,
      renderItem,
      itemHeight,
      visibleStart: 0,
      visibleEnd: 0,
      scrollTop: 0,
      containerHeight: container.clientHeight,
      visibleCount: Math.ceil(container.clientHeight / itemHeight),
      buffer: 5 // Buffer item count
    };

    this.virtualScrollers.set(container, scroller);

    // Create scroll container
    const scrollContainer = document.createElement('div');
    scrollContainer.style.height = `${items.length * itemHeight}px`;
    scrollContainer.style.position = 'relative';

    const visibleContainer = document.createElement('div');
    visibleContainer.style.position = 'absolute';
    visibleContainer.style.top = '0';
    visibleContainer.style.width = '100%';

    scrollContainer.appendChild(visibleContainer);
    container.appendChild(scrollContainer);

    // Scroll event handler
    const handleScroll = this.throttle(() => {
      this.updateVirtualScroller(container);
    }, 16); // 60fps

    container.addEventListener('scroll', handleScroll);

    // Initial render
    this.updateVirtualScroller(container);

    return {
      update: (newItems) => {
        scroller.items = newItems;
        scrollContainer.style.height = `${newItems.length * itemHeight}px`;
        this.updateVirtualScroller(container);
      },
      destroy: () => {
        container.removeEventListener('scroll', handleScroll);
        this.virtualScrollers.delete(container);
      }
    };
  }

  updateVirtualScroller(container) {
    const scroller = this.virtualScrollers.get(container);
    if (!scroller) return;

    const scrollTop = container.scrollTop;
    const visibleStart = Math.max(0, Math.floor(scrollTop / scroller.itemHeight) - scroller.buffer);
    const visibleEnd = Math.min(
      scroller.items.length,
      visibleStart + scroller.visibleCount + scroller.buffer * 2
    );

    if (visibleStart !== scroller.visibleStart || visibleEnd !== scroller.visibleEnd) {
      scroller.visibleStart = visibleStart;
      scroller.visibleEnd = visibleEnd;

      const visibleContainer = container.querySelector('div > div');
      visibleContainer.innerHTML = '';
      visibleContainer.style.top = `${visibleStart * scroller.itemHeight}px`;

      for (let i = visibleStart; i < visibleEnd; i++) {
        const item = scroller.items[i];
        const element = scroller.renderItem(item, i);
        element.style.height = `${scroller.itemHeight}px`;
        visibleContainer.appendChild(element);
      }
    }
  }

  // Image lazy loading
  setupLazyLoading(selector = 'img[data-src]') {
    const images = document.querySelectorAll(selector);

    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          this.loadImage(img);
          imageObserver.unobserve(img);
        }
      });
    }, {
      rootMargin: '50px'
    });

    images.forEach(img => imageObserver.observe(img));
    this.observers.set('lazyImages', imageObserver);
  }

  async loadImage(img) {
    const src = img.dataset.src;
    if (!src) return;

    try {
      const loadPromise = new Promise((resolve, reject) => {
        const tempImg = new Image();
        tempImg.onload = () => resolve(tempImg);
        tempImg.onerror = reject;
        tempImg.src = src;
      });

      await loadPromise;
      img.src = src;
      img.classList.add('loaded');
      img.removeAttribute('data-src');
    } catch (error) {
      console.error('Image loading failed:', src, error);
      img.classList.add('error');
    }
  }

  // Component lazy rendering
  setupLazyComponents(selector = '[data-lazy-component]') {
    const components = document.querySelectorAll(selector);

    const componentObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const component = entry.target;
          this.loadComponent(component);
          componentObserver.unobserve(component);
        }
      });
    }, {
      rootMargin: '100px'
    });

    components.forEach(component => componentObserver.observe(component));
    this.observers.set('lazyComponents', componentObserver);
  }

  async loadComponent(element) {
    const componentName = element.dataset.lazyComponent;
    const componentProps = element.dataset.componentProps;

    try {
      element.classList.add('loading');

      // Dynamic component import
      const module = await import(`./components/${componentName}.js`);
      const Component = module.default || module[componentName];

      // Create component instance
      const props = componentProps ? JSON.parse(componentProps) : {};
      const instance = new Component(element, props);

      // Render component
      await instance.render();

      element.classList.remove('loading');
      element.classList.add('loaded');
    } catch (error) {
      console.error('Component loading failed:', componentName, error);
      element.classList.remove('loading');
      element.classList.add('error');
    }
  }

  // Batch DOM updates
  batchDOMUpdates(updates) {
    return new Promise(resolve => {
      requestAnimationFrame(() => {
        updates.forEach(update => update());
        resolve();
      });
    });
  }

  // Memory leak detection
  startMemoryMonitoring() {
    const memoryInfo = performance.memory;
    let lastUsedJSHeapSize = memoryInfo.usedJSHeapSize;

    setInterval(() => {
      const currentUsedJSHeapSize = performance.memory.usedJSHeapSize;
      const memoryDelta = currentUsedJSHeapSize - lastUsedJSHeapSize;

      if (memoryDelta > 10 * 1024 * 1024) { // 10MB increase
        console.warn('Memory usage growing too fast:', {
          delta: `${(memoryDelta / 1024 / 1024).toFixed(2)}MB`,
          current: `${(currentUsedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
          limit: `${(memoryInfo.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB`
        });
      }

      lastUsedJSHeapSize = currentUsedJSHeapSize;
    }, 10000); // Check every 10 seconds
  }

  // Performance metrics monitoring
  setupPerformanceMonitoring() {
    // Monitor long tasks
    if ('PerformanceObserver' in window) {
      const longTaskObserver = new PerformanceObserver(list => {
        list.getEntries().forEach(entry => {
          console.warn('Long task detected:', {
            duration: entry.duration,
            startTime: entry.startTime,
            name: entry.name
          });
        });
      });

      try {
        longTaskObserver.observe({ entryTypes: ['longtask'] });
      } catch (error) {
        // Some environments may not support longtask
      }
    }

    // Monitor resource loading
    window.addEventListener('load', () => {
      const navigation = performance.getEntriesByType('navigation')[0];
      const resources = performance.getEntriesByType('resource');

      console.log('Page performance metrics:', {
        domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
        loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
        resourceCount: resources.length,
        totalSize: resources.reduce((sum, resource) => sum + (resource.transferSize || 0), 0)
      });
    });
  }

  setupResourceOptimization() {
    // Preload critical resources
    this.preloadCriticalResources([
      { href: 'icons/icon48.png', as: 'image' },
      { href: 'css/critical.css', as: 'style' }
    ]);

    // Preconnect external domains
    this.preconnectDomains([
      'https://fonts.googleapis.com',
      'https://api.example.com'
    ]);
  }

  preloadCriticalResources(resources) {
    resources.forEach(resource => {
      const link = document.createElement('link');
      link.rel = 'preload';
      link.href = chrome.runtime.getURL(resource.href);
      link.as = resource.as;
      if (resource.type) link.type = resource.type;
      document.head.appendChild(link);
    });
  }

  preconnectDomains(domains) {
    domains.forEach(domain => {
      const link = document.createElement('link');
      link.rel = 'preconnect';
      link.href = domain;
      document.head.appendChild(link);
    });
  }

  setupIntersectionObserver() {
    // Set up observer for animation elements
    const animationObserver = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.classList.add('animate-in');
        }
      });
    }, {
      threshold: 0.1
    });

    const animatableElements = document.querySelectorAll('[data-animate]');
    animatableElements.forEach(el => animationObserver.observe(el));
    this.observers.set('animation', animationObserver);
  }

  // Cleanup resources
  cleanup() {
    this.observers.forEach(observer => observer.disconnect());
    this.observers.clear();

    this.debounceTimers.forEach(timer => clearTimeout(timer));
    this.debounceTimers.clear();

    this.throttleTimers.forEach(timer => clearTimeout(timer));
    this.throttleTimers.clear();

    this.virtualScrollers.clear();
    this.imageLoaders.clear();
  }

  // Get performance report
  getPerformanceReport() {
    const navigation = performance.getEntriesByType('navigation')[0];
    const resources = performance.getEntriesByType('resource');
    const memory = performance.memory;

    return {
      timing: {
        domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
        loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
        firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0,
        firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0
      },
      resources: {
        count: resources.length,
        totalSize: resources.reduce((sum, r) => sum + (r.transferSize || 0), 0),
        slowResources: resources.filter(r => r.duration > 1000)
      },
      memory: {
        used: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
        total: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)}MB`,
        limit: `${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)}MB`
      },
      observers: {
        active: this.observers.size,
        virtualScrollers: this.virtualScrollers.size
      }
    };
  }
}

// Global performance optimizer instance
const performanceOptimizer = new PerformanceOptimizer();

// Export for use by other modules
window.performanceOptimizer = performanceOptimizer;

11.4 Accessibility

11.4.1 Accessibility Support Implementation

// accessibility.js - Accessibility support
class AccessibilityManager {
  constructor() {
    this.focusableElements = [
      'button',
      'input',
      'select',
      'textarea',
      'a[href]',
      '[tabindex]:not([tabindex="-1"])',
      '[contenteditable="true"]'
    ];
    this.announcements = [];
    this.init();
  }

  init() {
    this.setupKeyboardNavigation();
    this.setupScreenReaderSupport();
    this.setupFocusManagement();
    this.setupAriaLiveRegion();
    this.enhanceFormAccessibility();
  }

  setupKeyboardNavigation() {
    document.addEventListener('keydown', (e) => {
      this.handleGlobalKeyboard(e);
    });

    // Add focus indicators to all focusable elements
    const focusableSelectors = this.focusableElements.join(', ');
    document.addEventListener('focusin', (e) => {
      if (e.target.matches(focusableSelectors)) {
        e.target.classList.add('keyboard-focused');
      }
    });

    document.addEventListener('focusout', (e) => {
      e.target.classList.remove('keyboard-focused');
    });
  }

  handleGlobalKeyboard(e) {
    // Escape key handling
    if (e.key === 'Escape') {
      this.handleEscapeKey();
    }

    // Tab key trap
    if (e.key === 'Tab') {
      this.handleTabKey(e);
    }

    // Arrow key navigation
    if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
      this.handleArrowKeys(e);
    }

    // Shortcut key handling
    this.handleShortcuts(e);
  }

  handleEscapeKey() {
    // Close modal
    const modal = document.querySelector('.ce-modal-overlay.visible');
    if (modal) {
      const closeBtn = modal.querySelector('.ce-modal-close');
      closeBtn?.click();
      return;
    }

    // Close dropdown menu
    const dropdown = document.querySelector('.dropdown.open');
    if (dropdown) {
      dropdown.classList.remove('open');
      return;
    }

    // Clear focus
    if (document.activeElement && document.activeElement !== document.body) {
      document.activeElement.blur();
    }
  }

  handleTabKey(e) {
    const modal = document.querySelector('.ce-modal-overlay.visible');
    if (modal) {
      this.trapFocusInModal(e, modal);
    }
  }

  trapFocusInModal(e, modal) {
    const focusableElements = modal.querySelectorAll(this.focusableElements.join(', '));
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    if (e.shiftKey) {
      if (document.activeElement === firstElement) {
        e.preventDefault();
        lastElement.focus();
      }
    } else {
      if (document.activeElement === lastElement) {
        e.preventDefault();
        firstElement.focus();
      }
    }
  }

  handleArrowKeys(e) {
    const target = e.target;

    // Menu navigation
    if (target.closest('.menu, .dropdown-menu')) {
      this.handleMenuNavigation(e);
    }

    // Table navigation
    if (target.closest('table')) {
      this.handleTableNavigation(e);
    }

    // Grid navigation
    if (target.closest('.grid[role="grid"]')) {
      this.handleGridNavigation(e);
    }
  }

  handleMenuNavigation(e) {
    const menu = e.target.closest('.menu, .dropdown-menu');
    const items = menu.querySelectorAll('[role="menuitem"]');
    const currentIndex = Array.from(items).indexOf(e.target);

    let nextIndex;
    switch (e.key) {
      case 'ArrowDown':
        nextIndex = (currentIndex + 1) % items.length;
        break;
      case 'ArrowUp':
        nextIndex = (currentIndex - 1 + items.length) % items.length;
        break;
      case 'Home':
        nextIndex = 0;
        break;
      case 'End':
        nextIndex = items.length - 1;
        break;
      default:
        return;
    }

    e.preventDefault();
    items[nextIndex].focus();
  }

  handleTableNavigation(e) {
    const table = e.target.closest('table');
    const cells = table.querySelectorAll('td, th');
    const currentCell = e.target.closest('td, th');
    const currentIndex = Array.from(cells).indexOf(currentCell);

    const row = currentCell.parentElement;
    const cellsInRow = row.querySelectorAll('td, th');
    const columnIndex = Array.from(cellsInRow).indexOf(currentCell);

    let nextCell;
    switch (e.key) {
      case 'ArrowRight':
        nextCell = cells[currentIndex + 1];
        break;
      case 'ArrowLeft':
        nextCell = cells[currentIndex - 1];
        break;
      case 'ArrowDown':
        const nextRow = row.nextElementSibling;
        if (nextRow) {
          nextCell = nextRow.querySelectorAll('td, th')[columnIndex];
        }
        break;
      case 'ArrowUp':
        const prevRow = row.previousElementSibling;
        if (prevRow) {
          nextCell = prevRow.querySelectorAll('td, th')[columnIndex];
        }
        break;
      default:
        return;
    }

    if (nextCell) {
      e.preventDefault();
      nextCell.focus();
    }
  }

  handleShortcuts(e) {
    // Alt + shortcuts
    if (e.altKey) {
      switch (e.key) {
        case 's':
          e.preventDefault();
          this.skipToMainContent();
          break;
        case 'h':
          e.preventDefault();
          this.skipToHeader();
          break;
        case 'f':
          e.preventDefault();
          this.skipToFooter();
          break;
      }
    }

    // Ctrl/Cmd + shortcuts
    if (e.ctrlKey || e.metaKey) {
      switch (e.key) {
        case '/':
          e.preventDefault();
          this.focusSearchInput();
          break;
        case 'k':
          e.preventDefault();
          this.openCommandPalette();
          break;
      }
    }
  }

  setupScreenReaderSupport() {
    // Dynamic content change notifications
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          this.announceContentChanges(mutation);
        }
      });
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: false
    });

    // Add appropriate ARIA attributes to dynamic content
    this.enhanceAriaLabels();
  }

  announceContentChanges(mutation) {
    const addedNodes = Array.from(mutation.addedNodes);
    const importantChanges = addedNodes.filter(node => {
      return node.nodeType === Node.ELEMENT_NODE &&
             (node.matches('.alert, .notification, .error, .success') ||
              node.hasAttribute('aria-live'));
    });

    importantChanges.forEach(node => {
      const text = node.textContent.trim();
      if (text) {
        this.announce(text, 'polite');
      }
    });
  }

  enhanceAriaLabels() {
    // Add labels to unlabeled form controls
    const unlabeledInputs = document.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
    unlabeledInputs.forEach(input => {
      const label = input.closest('.form-group, .input-group')?.querySelector('label');
      if (label && !label.getAttribute('for')) {
        const id = input.id || `input_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
        input.id = id;
        label.setAttribute('for', id);
      } else if (!label) {
        const placeholder = input.getAttribute('placeholder');
        if (placeholder) {
          input.setAttribute('aria-label', placeholder);
        }
      }
    });

    // Add descriptive labels to buttons
    const iconButtons = document.querySelectorAll('button:not([aria-label]):not([aria-labelledby])');
    iconButtons.forEach(button => {
      if (!button.textContent.trim()) {
        const icon = button.querySelector('.icon, [class*="icon"]');
        if (icon) {
          const action = this.guessButtonAction(button);
          button.setAttribute('aria-label', action);
        }
      }
    });
  }

  guessButtonAction(button) {
    const classes = button.className.toLowerCase();
    const iconClasses = button.querySelector('.icon, [class*="icon"]')?.className.toLowerCase() || '';

    if (classes.includes('close') || iconClasses.includes('close')) return 'Close';
    if (classes.includes('delete') || iconClasses.includes('delete')) return 'Delete';
    if (classes.includes('edit') || iconClasses.includes('edit')) return 'Edit';
    if (classes.includes('save') || iconClasses.includes('save')) return 'Save';
    if (classes.includes('search') || iconClasses.includes('search')) return 'Search';
    if (classes.includes('menu') || iconClasses.includes('menu')) return 'Menu';

    return 'Button';
  }

  setupFocusManagement() {
    // Set initial focus on page load
    window.addEventListener('load', () => {
      this.setInitialFocus();
    });

    // Focus management for dynamic content
    document.addEventListener('ce:content-loaded', (e) => {
      this.manageDynamicFocus(e.detail.container);
    });
  }

  setInitialFocus() {
    // Find first error field
    const errorField = document.querySelector('.error input, .error select, .error textarea');
    if (errorField) {
      errorField.focus();
      return;
    }

    // Find primary action button
    const primaryAction = document.querySelector('.btn-primary, [role="button"][data-primary]');
    if (primaryAction) {
      primaryAction.focus();
      return;
    }

    // Find first form field
    const firstInput = document.querySelector('input, select, textarea');
    if (firstInput) {
      firstInput.focus();
      return;
    }
  }

  manageDynamicFocus(container) {
    // Set appropriate focus for new content
    const focusableElement = container.querySelector(this.focusableElements.join(', '));
    if (focusableElement) {
      focusableElement.focus();
    }
  }

  setupAriaLiveRegion() {
    // Create live region for notifications
    const liveRegion = document.createElement('div');
    liveRegion.setAttribute('aria-live', 'polite');
    liveRegion.setAttribute('aria-atomic', 'true');
    liveRegion.className = 'sr-only';
    liveRegion.id = 'aria-live-region';
    document.body.appendChild(liveRegion);

    // Create live region for urgent notifications
    const urgentRegion = document.createElement('div');
    urgentRegion.setAttribute('aria-live', 'assertive');
    urgentRegion.setAttribute('aria-atomic', 'true');
    urgentRegion.className = 'sr-only';
    urgentRegion.id = 'aria-live-urgent';
    document.body.appendChild(urgentRegion);
  }

  announce(message, priority = 'polite') {
    const regionId = priority === 'assertive' ? 'aria-live-urgent' : 'aria-live-region';
    const region = document.getElementById(regionId);

    if (region) {
      // Clear previous message
      region.textContent = '';

      // Delay setting new message to ensure screen readers detect the change
      setTimeout(() => {
        region.textContent = message;
      }, 100);

      // Record announcement history
      this.announcements.push({
        message,
        priority,
        timestamp: Date.now()
      });

      // Limit history length
      if (this.announcements.length > 50) {
        this.announcements = this.announcements.slice(-25);
      }
    }
  }

  enhanceFormAccessibility() {
    // Add fieldset and legend to forms
    const forms = document.querySelectorAll('form');
    forms.forEach(form => {
      this.addFormStructure(form);
      this.addFormValidation(form);
    });
  }

  addFormStructure(form) {
    // Check if there's already appropriate structure
    if (form.querySelector('fieldset')) return;

    const inputs = form.querySelectorAll('input, select, textarea');
    if (inputs.length > 3) {
      // Create fieldset to wrap multiple related fields
      const fieldset = document.createElement('fieldset');
      const legend = document.createElement('legend');
      legend.textContent = form.getAttribute('data-form-title') || 'Form';

      fieldset.appendChild(legend);

      // Move all child elements into fieldset
      while (form.firstChild) {
        fieldset.appendChild(form.firstChild);
      }

      form.appendChild(fieldset);
    }
  }

  addFormValidation(form) {
    form.addEventListener('submit', (e) => {
      const isValid = this.validateForm(form);
      if (!isValid) {
        e.preventDefault();
        this.focusFirstError(form);
        this.announce('Form validation failed, please check error fields', 'assertive');
      }
    });

    // Real-time validation
    const inputs = form.querySelectorAll('input, select, textarea');
    inputs.forEach(input => {
      input.addEventListener('blur', () => {
        this.validateField(input);
      });
    });
  }

  validateForm(form) {
    const inputs = form.querySelectorAll('input, select, textarea');
    let isValid = true;

    inputs.forEach(input => {
      if (!this.validateField(input)) {
        isValid = false;
      }
    });

    return isValid;
  }

  validateField(input) {
    const value = input.value.trim();
    const required = input.hasAttribute('required');
    let isValid = true;
    let errorMessage = '';

    if (required && !value) {
      isValid = false;
      errorMessage = 'This field is required';
    } else if (input.type === 'email' && value && !this.isValidEmail(value)) {
      isValid = false;
      errorMessage = 'Please enter a valid email address';
    }

    this.setFieldError(input, isValid ? null : errorMessage);
    return isValid;
  }

  setFieldError(input, errorMessage) {
    const container = input.closest('.form-group, .input-group, .ce-input-group');
    if (!container) return;

    // Remove previous error state
    container.classList.remove('error');
    const existingError = container.querySelector('.field-error');
    if (existingError) {
      existingError.remove();
    }

    if (errorMessage) {
      // Add error state
      container.classList.add('error');

      // Create error message element
      const errorElement = document.createElement('div');
      errorElement.className = 'field-error';
      errorElement.textContent = errorMessage;
      errorElement.setAttribute('role', 'alert');

      // Associate error message to input field
      const errorId = `error_${input.id || Date.now()}`;
      errorElement.id = errorId;
      input.setAttribute('aria-describedby', errorId);

      container.appendChild(errorElement);
    } else {
      input.removeAttribute('aria-describedby');
    }
  }

  focusFirstError(form) {
    const errorField = form.querySelector('.error input, .error select, .error textarea');
    if (errorField) {
      errorField.focus();
      errorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
  }

  // Convenience methods
  skipToMainContent() {
    const main = document.querySelector('main, [role="main"], #main');
    if (main) {
      main.focus();
      main.scrollIntoView({ behavior: 'smooth' });
    }
  }

  skipToHeader() {
    const header = document.querySelector('header, [role="banner"], .header');
    if (header) {
      header.focus();
      header.scrollIntoView({ behavior: 'smooth' });
    }
  }

  skipToFooter() {
    const footer = document.querySelector('footer, [role="contentinfo"], .footer');
    if (footer) {
      footer.focus();
      footer.scrollIntoView({ behavior: 'smooth' });
    }
  }

  focusSearchInput() {
    const searchInput = document.querySelector('input[type="search"], input[placeholder*="Search"], #search');
    if (searchInput) {
      searchInput.focus();
    }
  }

  openCommandPalette() {
    // Command palette opening logic
    this.announce('Command palette opened', 'polite');
  }

  isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  // Get accessibility status report
  getAccessibilityReport() {
    const issues = this.checkAccessibilityIssues();

    return {
      score: this.calculateAccessibilityScore(issues),
      issues: issues,
      announcements: this.announcements.length,
      focusableElements: document.querySelectorAll(this.focusableElements.join(', ')).length
    };
  }

  checkAccessibilityIssues() {
    const issues = [];

    // Check images missing alt attributes
    const imagesWithoutAlt = document.querySelectorAll('img:not([alt])');
    if (imagesWithoutAlt.length > 0) {
      issues.push({
        type: 'missing-alt',
        count: imagesWithoutAlt.length,
        description: 'Images missing alt attributes'
      });
    }

    // Check unlabeled form controls
    const unlabeledInputs = document.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
    if (unlabeledInputs.length > 0) {
      issues.push({
        type: 'unlabeled-input',
        count: unlabeledInputs.length,
        description: 'Form controls missing labels'
      });
    }

    // Check color contrast (simple check)
    const lowContrastElements = this.findLowContrastElements();
    if (lowContrastElements.length > 0) {
      issues.push({
        type: 'low-contrast',
        count: lowContrastElements.length,
        description: 'Insufficient color contrast'
      });
    }

    return issues;
  }

  findLowContrastElements() {
    // Simple contrast check implementation
    const elements = document.querySelectorAll('*');
    const lowContrast = [];

    elements.forEach(el => {
      const style = window.getComputedStyle(el);
      const color = style.color;
      const backgroundColor = style.backgroundColor;

      // This requires a more complex contrast calculation algorithm
      // Simplified version for demonstration only
      if (color && backgroundColor && this.hasLowContrast(color, backgroundColor)) {
        lowContrast.push(el);
      }
    });

    return lowContrast.slice(0, 10); // Limit count
  }

  hasLowContrast(color, backgroundColor) {
    // Simplified contrast check
    // Actual implementation needs more complex color analysis
    return false; // Placeholder
  }

  calculateAccessibilityScore(issues) {
    let score = 100;

    issues.forEach(issue => {
      switch (issue.type) {
        case 'missing-alt':
          score -= issue.count * 5;
          break;
        case 'unlabeled-input':
          score -= issue.count * 10;
          break;
        case 'low-contrast':
          score -= issue.count * 3;
          break;
      }
    });

    return Math.max(0, score);
  }
}

// Global accessibility manager instance
const accessibilityManager = new AccessibilityManager();

// Export for use by other modules
window.accessibilityManager = accessibilityManager;

warning Accessibility Requirements Ensure Chrome extensions meet WCAG 2.1 AA level standards:

  • All interactive elements are keyboard accessible
  • Provide appropriate ARIA labels and descriptions
  • Ensure sufficient color contrast
  • Support screen readers
  • Provide skip links and navigation shortcuts

tip Best Practices

  1. Progressive Enhancement: Ensure core functionality works in all environments
  2. Performance First: Optimize critical rendering path and interaction response time
  3. User Testing: Conduct usability testing with real users
  4. Internationalization: Support multiple languages and different cultural backgrounds
  5. Error Handling: Provide clear error messages and recovery paths

note Summary This chapter comprehensively introduced user interface design and experience optimization for Chrome extensions, including modern UI framework implementation, responsive design, performance optimization, and accessibility support. Excellent user experience is a key factor in extension success. In the next chapter, we will learn about security and permission management to ensure the safety of the extension and user data.

Categories