← Accessible Components results

Accessible Components

Qwen 3.6 27b MTP · composer

Initial view of Accessible Components

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 40,638 bytes · SHA-256 326fab195060
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessible Component Showcase</title>
<style>
/* ── css: base ── */
/* === Tunable Constants === */
:root {
  /* Colors */
  --color-primary: #4f46e5;
  --color-primary-hover: #4338ca;
  --color-primary-light: #eef2ff;
  --color-secondary: #6b7280;
  --color-secondary-hover: #4b5563;
  --color-bg: #ffffff;
  --color-bg-alt: #f9fafb;
  --color-text: #111827;
  --color-text-muted: #6b7280;
  --color-border: #e5e7eb;
  --color-border-focus: #4f46e5;
  --color-danger: #ef4444;
  --color-success: #10b981;

  /* Spacing */
  --space-xs: 0.25rem;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --space-xl: 2rem;
  --space-2xl: 3rem;

  /* Typography */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --text-xl: 1.5rem;
  --text-2xl: 2rem;

  /* Animation */
  --duration-fast: 100ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Layout */
  --sidebar-width: 240px;
  --max-content-width: 800px;
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
}

/* === Reset & Base === */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; font-size: 16px; }

body {
  font-family: var(--font-sans);
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* === Reduced Motion === */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  html { scroll-behavior: auto; }
}

/* === Screen Reader Only === */
.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;
}

/* === Skip Link === */
.skip-link {
  position: absolute; top: -100%; left: var(--space-md); z-index: 9999;
  padding: var(--space-sm) var(--space-md); background: var(--color-primary);
  color: #fff; text-decoration: none; font-weight: 600; border-radius: var(--radius-sm);
  transition: top var(--duration-fast) ease;
}
.skip-link:focus {
  top: var(--space-sm);
}

/* === Focus Indicators (Global) === */
:focus-visible {
  outline: 2px solid var(--color-border-focus);
  outline-offset: 2px;
}
button:focus-visible, [role="tab"]:focus-visible, [role="menuitem"]:focus-visible,
input:focus-visible, a:focus-visible {
  outline: 2px solid var(--color-border-focus);
  outline-offset: 2px;
}

/* === Buttons === */
.btn {
  display: inline-flex; align-items: center; gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg); font-size: var(--text-sm);
  font-weight: 500; border-radius: var(--radius-md); cursor: pointer;
  border: 1px solid transparent; transition: background var(--duration-fast), box-shadow var(--duration-fast);
  line-height: 1.5; white-space: nowrap;
}
.btn-primary {
  background: var(--color-primary); color: #fff; border-color: var(--color-primary);
}
.btn-primary:hover { background: var(--color-primary-hover); }
.btn-secondary {
  background: var(--color-bg-alt); color: var(--color-text); border-color: var(--color-border);
}
.btn-secondary:hover { background: #e5e7eb; }

/* === Kbd Hint === */
kbd {
  display: inline-block; padding: 1px 6px; font-family: var(--font-mono);
  font-size: var(--text-xs); background: var(--color-bg-alt); border: 1px solid var(--color-border);
  border-radius: var(--radius-sm); color: var(--color-text-muted);
}

/* === Toast === */
.toast {
  position: fixed; bottom: var(--space-lg); right: var(--space-lg); z-index: 9000;
  padding: var(--space-md) var(--space-lg); background: #1f2937; color: #fff;
  border-radius: var(--radius-md); font-size: var(--text-sm); box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  animation: toastIn var(--duration-normal) var(--ease-out);
}
@keyframes toastIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
/* ── css: layout ── */
/* === Layout === */
#app {
  display: flex; min-height: 100vh;
}

.sidebar {
  position: fixed; top: 0; left: 0; bottom: 0; width: var(--sidebar-width);
  background: var(--color-bg-alt); border-right: 1px solid var(--color-border);
  padding: var(--space-xl) 0; overflow-y: auto; z-index: 100;
}

.sidebar-brand {
  padding: 0 var(--space-lg) var(--space-xl); font-size: var(--text-lg);
  font-weight: 700; color: var(--color-primary); display: flex; align-items: center; gap: var(--space-sm);
}
.brand-icon { font-size: var(--text-xl); }

.sidebar-nav { list-style: none; }
.sidebar-nav li a {
  display: block; padding: var(--space-sm) var(--space-lg); color: var(--color-text-muted);
  text-decoration: none; font-size: var(--text-sm); border-left: 3px solid transparent;
  transition: color var(--duration-fast), background var(--duration-fast), border-color var(--duration-fast);
}
.sidebar-nav li a:hover { color: var(--color-text); background: rgba(0,0,0,0.04); }
.sidebar-nav li a:focus-visible { outline-offset: -2px; }

.main-content {
  margin-left: var(--sidebar-width); flex: 1; max-width: calc(var(--sidebar-width) + var(--max-content-width));
}

.page-header {
  padding: var(--space-2xl) var(--space-2xl) var(--space-xl);
  border-bottom: 1px solid var(--color-border); background: var(--color-bg);
}
.page-header h1 { font-size: var(--text-2xl); font-weight: 700; margin-bottom: var(--space-sm); }
.page-header p { color: var(--color-text-muted); max-width: 60ch; line-height: 1.7; }

/* === Component Sections === */
.component-section {
  padding: var(--space-2xl) var(--space-xl); border-bottom: 1px solid var(--color-border);
}
.component-section h2 {
  font-size: var(--text-xl); font-weight: 600; margin-bottom: var(--space-lg);
  scroll-margin-top: var(--space-xl);
}

.demo-area {
  background: var(--color-bg-alt); border: 1px solid var(--color-border);
  border-radius: var(--radius-lg); padding: var(--space-xl); margin-bottom: var(--space-md);
}

.kbd-hint {
  font-size: var(--text-xs); color: var(--color-text-muted); font-family: var(--font-mono);
  margin-top: var(--space-sm);
}

/* === Panel Content Styles === */
.tabpanel h3, .tabpanel dl dt { font-weight: 600; margin-bottom: var(--space-sm); }
.tabpanel p { margin-bottom: var(--space-md); color: var(--color-text-muted); line-height: 1.7; }
.tabpanel ul { list-style: disc; padding-left: var(--space-lg); margin-bottom: var(--space-md); }
.tabpanel li { margin-bottom: var(--space-xs); color: var(--color-text-muted); }
.tabpanel dd { margin-bottom: var(--space-sm); color: var(--color-text-muted); padding-left: var(--space-md); }

/* === Responsive === */
@media (max-width: 768px) {
  .sidebar { display: none; }
  .main-content { margin-left: 0; }
}
/* ── css: components ── */
/* === Modal Dialog === */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 1000; background: rgba(0, 0, 0, 0.5);
  animation: fadeIn var(--duration-normal) ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.modal-dialog {
  position: fixed; inset: 0; z-index: 1001; display: flex; align-items: center; justify-content: center;
  padding: var(--space-lg); animation: modalIn var(--duration-normal) var(--ease-out);
}
@keyframes modalIn { from { opacity: 0; transform: scale(0.95) translateY(-8px); } to { opacity: 1; transform: scale(1) translateY(0); } }

.modal-content {
  background: var(--color-bg); border-radius: var(--radius-lg); padding: var(--space-xl);
  max-width: 480px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.modal-content h2 { font-size: var(--text-lg); margin-bottom: var(--space-md); }
.modal-content p { color: var(--color-text-muted); margin-bottom: var(--space-md); line-height: 1.7; }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--space-sm); margin-top: var(--space-lg); }

/* === Dropdown Menu === */
.dropdown-container { position: relative; display: inline-block; }

.dropdown-trigger { font-size: var(--text-sm); }

.dropdown-menu {
  position: absolute; top: calc(100% + 4px); left: 0; min-width: 200px;
  background: var(--color-bg); border: 1px solid var(--color-border);
  border-radius: var(--radius-md); box-shadow: 0 4px 16px rgba(0,0,0,0.1);
  padding: var(--space-xs) 0; list-style: none; z-index: 50;
  animation: dropdownIn var(--duration-fast) ease;
}
@keyframes dropdownIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }

.dropdown-menu [role="menuitem"] {
  padding: var(--space-sm) var(--space-lg); cursor: pointer; font-size: var(--text-sm);
  display: block; width: 100%; text-align: left; background: none; border: none;
}
.dropdown-menu [role="menuitem"]:hover,
.dropdown-menu [role="menuitem"][aria-selected="true"] {
  background: var(--color-primary-light); color: var(--color-primary);
}

.dropdown-menu [role="separator"] {
  height: 1px; margin: var(--space-xs) 0; padding: 0;
  background: var(--color-border); list-style: none;
}

/* === Tabs === */
.tabs-container {
  display: flex; border-bottom: 2px solid var(--color-border); gap: 0;
  margin-bottom: var(--space-lg);
}

.tab-btn {
  padding: var(--space-sm) var(--space-lg); font-size: var(--text-sm); font-weight: 500;
  background: none; border: none; cursor: pointer; color: var(--color-text-muted);
  position: relative; transition: color var(--duration-fast);
}
.tab-btn:hover { color: var(--color-text); }

.tab-active, .tab-btn[aria-selected="true"] {
  color: var(--color-primary); font-weight: 600;
}
.tab-btn::after {
  content: ''; position: absolute; bottom: -2px; left: 0; right: 0; height: 2px;
  background: var(--color-primary); transform: scaleX(0); transition: transform var(--duration-fast) ease;
}
.tab-active::after, .tab-btn[aria-selected="true"]::after { transform: scaleX(1); }

.tabpanel { padding: var(--space-md) 0; animation: panelIn var(--duration-normal) ease; }
@keyframes panelIn { from { opacity: 0; } to { opacity: 1; } }

/* === Combobox === */
.combobox-wrapper { position: relative; max-width: 400px; width: 100%; }

.combobox-input {
  width: 100%; padding: var(--space-sm) var(--space-md); font-size: var(--text-sm);
  border: 1px solid var(--color-border); border-radius: var(--radius-md);
  background: var(--color-bg); color: var(--color-text); transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
}
.combobox-input:focus {
  border-color: var(--color-primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
}

#combobox-listbox {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; max-height: 240px;
  overflow-y: auto; background: var(--color-bg); border: 1px solid var(--color-border);
  border-radius: var(--radius-md); box-shadow: 0 4px 16px rgba(0,0,0,0.1);
  list-style: none; padding: var(--space-xs) 0; z-index: 50;
}

#combobox-listbox li {
  padding: var(--space-sm) var(--space-md); font-size: var(--text-sm); cursor: pointer;
}
#combobox-listbox li[aria-selected="true"] {
  background: var(--color-primary-light); color: var(--color-primary);
}

/* === Accordion === */
.accordion-group { max-width: 600px; }

.accordion-item + .accordion-item { border-top: 1px solid var(--color-border); }

.accordion-trigger {
  display: flex; align-items: center; justify-content: space-between; width: 100%;
  padding: var(--space-md) 0; font-size: var(--text-base); font-weight: 500;
  background: none; border: none; cursor: pointer; text-align: left; color: var(--color-text);
  line-height: 1.5; gap: var(--space-md);
}
.accordion-trigger::after {
  content: '+'; font-size: var(--text-lg); font-weight: 400; color: var(--color-text-muted);
  flex-shrink: 0; transition: transform var(--duration-normal) ease;
}
.accordion-trigger[aria-expanded="true"]::after { transform: rotate(45deg); }

.accordion-panel { overflow: hidden; }
.accordion-panel p { padding: 0 0 var(--space-lg); color: var(--color-text-muted); line-height: 1.7; }

/* Accordion animation */
.accordion-panel.animating { transition: height var(--duration-slow) ease; }
</style>
</head>
<body>
<a class="skip-link" id="skip-link" href="#main-content">Skip to main content</a>
<nav class="sidebar" aria-label="Component navigation">
  <div class="sidebar-brand"><span class="brand-icon">◆</span> A11y Components</div>
  <ul class="sidebar-nav">
    <li><a href="#modal-section">Modal Dialog</a></li>
    <li><a href="#dropdown-section">Dropdown Menu</a></li>
    <li><a href="#tabs-section">Tabs</a></li>
    <li><a href="#combobox-section">Combobox</a></li>
    <li><a href="#accordion-section">Accordion</a></li>
  </ul>
</nav>
<main id="main-content" class="main-content">
  <header class="page-header">
    <h1>Accessible Component Showcase</h1>
    <p>A vanilla JS component library where every widget is genuinely keyboard- and screen-reader-accessible, following WAI-ARIA Authoring Practices.</p>
  </header>

  <!-- Modal Dialog Section -->
  <section id="modal-section" class="component-section">
    <h2>Modal Dialog</h2>
    <div class="demo-area">
      <button type="button" class="btn btn-primary" id="open-modal-btn">Open Dialog</button>
      <p class="kbd-hint">Tab/Shift+Tab cycle · Esc close · Click backdrop close</p>
    </div>
  </section>

  <!-- Dropdown Menu Section -->
  <section id="dropdown-section" class="component-section">
    <h2>Dropdown Menu</h2>
    <div class="demo-area">
      <div class="dropdown-container" id="dropdown-demo">
        <button type="button" class="btn btn-secondary dropdown-trigger" aria-haspopup="menu" aria-expanded="false" id="dropdown-btn">Menu Actions ▾</button>
        <ul class="dropdown-menu" role="menu" aria-labelledby="dropdown-btn" id="dropdown-menu-list" hidden="">
          <li role="menuitem" tabindex="-1" data-action="copy">Copy</li>
          <li role="menuitem" tabindex="-1" data-action="paste">Paste</li>
          <li role="separator" aria-orientation="horizontal"></li>
          <li role="menuitem" tabindex="-1" data-action="bold"><strong>Bold</strong></li>
          <li role="menuitem" tabindex="-1" data-action="italic"><em>Italic</em></li>
          <li role="separator" aria-orientation="horizontal"></li>
          <li role="menuitem" tabindex="-1" data-action="delete">Delete…</li>
        </ul>
      </div>
      <p class="kbd-hint">↓ open &amp; focus first · ↑↓ navigate (wrapping) · Home/End jump · Enter select · Esc close</p>
    </div>
  </section>

  <!-- Tabs Section -->
  <section id="tabs-section" class="component-section">
    <h2>Tabs</h2>
    <div class="demo-area">
      <div class="tabs-container" role="tablist" aria-label="Project information" id="tabs-demo">
        <button role="tab" aria-selected="true" tabindex="0" aria-controls="tabpanel-overview" id="tab-overview" class="tab-btn tab-active">Overview</button>
        <button role="tab" aria-selected="false" tabindex="-1" aria-controls="tabpanel-features" id="tab-features" class="tab-btn">Features</button>
        <button role="tab" aria-selected="false" tabindex="-1" aria-controls="tabpanel-changelog" id="tab-changelog" class="tab-btn">Changelog</button>
      </div>
      <div role="tabpanel" id="tabpanel-overview" aria-labelledby="tab-overview" class="tabpanel">
        <h3>Project Overview</h3>
        <p>This component library is built from scratch with accessibility as a first-class concern. Every interaction pattern follows the WAI-ARIA Authoring Practices Guide, ensuring that users of assistive technologies have an experience equal to mouse users.</p>
        <p>The library uses zero external dependencies — just vanilla JavaScript and hand-written CSS that respects user preferences like reduced motion.</p>
      </div>
      <div role="tabpanel" id="tabpanel-features" aria-labelledby="tab-features" class="tabpanel" hidden="">
        <h3>Key Features</h3>
        <ul>
          <li><strong>Focus management:</strong> Focus trapping in modals, roving tabindex in tablists and menus.</li>
          <li><strong>Semantic HTML:</strong> Proper roles, states, and properties on every interactive element.</li>
          <li><strong>Keyboard support:</strong> Full keyboard operability with logical focus order.</li>
          <li><strong>Screen reader tested:</strong> All ARIA relationships point to real elements; no dangling references.</li>
        </ul>
      </div>
      <div role="tabpanel" id="tabpanel-changelog" aria-labelledby="tab-changelog" class="tabpanel" hidden="">
        <h3>Changelog</h3>
        <dl>
          <dt><strong>v1.0.0</strong></dt><dd>Initial release with Modal, Dropdown, Tabs, Combobox, and Accordion components.</dd>
          <dt><strong>v1.1.0</strong></dt><dd>Added focus trap utility, aria-activedescendant manager, and prefers-reduced-motion support.</dd>
          <dt><strong>v1.2.0</strong></dt><dd>Improved combobox filtering performance; added Home/End navigation to dropdown menus.</dd>
        </dl>
      </div>
    </div>
    <p class="kbd-hint">←→ switch tabs · Tab moves into panel content</p>
  </section>

  <!-- Combobox Section -->
  <section id="combobox-section" class="component-section">
    <h2>Combobox (Autocomplete)</h2>
    <div class="demo-area">
      <label for="combobox-input" class="sr-only">Select a programming language</label>
      <div class="combobox-wrapper" id="combobox-demo">
        <input type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="combobox-listbox" aria-activedescendant="" id="combobox-input" autocomplete="off" placeholder="Search languages…" class="combobox-input">
        <ul role="listbox" id="combobox-listbox" aria-label="Programming languages" hidden=""></ul>
      </div>
      <p class="kbd-hint">Type to filter · ↓↑ highlight · Enter select · Esc close &amp; revert</p>
    </div>
  </section>

  <!-- Accordion Section -->
  <section id="accordion-section" class="component-section">
    <h2>Accordion / Disclosure Group</h2>
    <div class="demo-area">
      <div class="accordion-group" id="accordion-demo">
        <div class="accordion-item">
          <button type="button" aria-expanded="false" aria-controls="acc-panel-1" id="acc-btn-1" class="accordion-trigger">What is this component library?</button>
          <div role="region" id="acc-panel-1" aria-labelledby="acc-btn-1" class="accordion-panel" hidden="">
            <p>This is a vanilla JavaScript accessible component library showcasing five common UI patterns: Modal Dialog, Dropdown Menu, Tabs, Combobox, and Accordion. Each component follows WAI-ARIA Authoring Practices.</p>
          </div>
        </div>
        <div class="accordion-item">
          <button type="button" aria-expanded="false" aria-controls="acc-panel-2" id="acc-btn-2" class="accordion-trigger">How is accessibility implemented?</button>
          <div role="region" id="acc-panel-2" aria-labelledby="acc-btn-2" class="accordion-panel" hidden="">
            <p>Accessibility is built in from the ground up. We use proper ARIA roles, states, and properties on every element. Focus management follows best practices: focus trapping for modals, roving tabindex for tablists and menus, and aria-activedescendant for comboboxes.</p>
          </div>
        </div>
        <div class="accordion-item">
          <button type="button" aria-expanded="false" aria-controls="acc-panel-3" id="acc-btn-3" class="accordion-trigger">Can I use this in production?</button>
          <div role="region" id="acc-panel-3" aria-labelledby="acc-btn-3" class="accordion-panel" hidden="">
            <p>This is a demonstration of accessible patterns. The code is well-organized with shared utilities and tunable constants, making it easy to adapt for production use. All components respect user preferences like reduced motion.</p>
          </div>
        </div>
        <div class="accordion-item">
          <button type="button" aria-expanded="false" aria-controls="acc-panel-4" id="acc-btn-4" class="accordion-trigger">What browsers are supported?</button>
          <div role="region" id="acc-panel-4" aria-labelledby="acc-btn-4" class="accordion-panel" hidden="">
            <p>All modern browsers that support CSS custom properties and ES2017+ JavaScript. The components use standard DOM APIs available in all evergreen browsers.</p>
          </div>
        </div>
      </div>
    </div>
    <p class="kbd-hint">Enter/Space toggle · Multiple panels may be open</p>
  </section>

  <!-- Toast notification area -->
  <div id="toast-container" aria-live="polite" aria-atomic="true"></div>
</main>

<!-- Modal Dialog (rendered outside main flow) -->
<div class="modal-backdrop" id="modal-backdrop" hidden=""></div>
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" class="modal-dialog" id="modal-dialog" hidden="">
  <div class="modal-content">
    <h2 id="modal-title">Confirm Action</h2>
    <p>This is a modal dialog. Focus is trapped within this dialog until it is closed. The page behind the dialog is inert and not focusable.</p>
    <p>You can close this dialog by pressing <kbd>Escape</kbd>, clicking the backdrop, or clicking the button below.</p>
    <div class="modal-actions">
      <button type="button" class="btn btn-secondary" id="modal-cancel-btn">Cancel</button>
      <button type="button" class="btn btn-primary" id="modal-confirm-btn">Confirm</button>
    </div>
  </div>
</div>
<script>
'use strict';

// ── function: getFocusableElements ──
// === Shared Utilities ===

/**
 * Get all focusable elements within a container.
 */
function getFocusableElements(container) {
  const selectors = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), details > summary';
  return Array.from(container.querySelectorAll(selectors)).filter(el => !el.hidden && el.offsetParent !== null);
}

// ── function: createFocusTrap ──
/**
 * Focus trap utility — keeps focus cycling within a container.
 */
function createFocusTrap(container) {
  const focusable = getFocusableElements(container);
  if (focusable.length === 0) return { activate: () => {}, deactivate: () => {} };

  let handler = null;

  function trapHandler(e) {
    if (e.key !== 'Tab') return;
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (e.shiftKey) {
      if (document.activeElement === first) { e.preventDefault(); last.focus(); }
    } else {
      if (document.activeElement === last) { e.preventDefault(); first.focus(); }
    }
  }

  return {
    activate() { handler = trapHandler; document.addEventListener('keydown', handler); },
    deactivate() { if (handler) { document.removeEventListener('keydown', handler); handler = null; } }
  };
}

// ── function: showToast ──
/**
 * Show a toast notification.
 */
function showToast(message, duration = 2500) {
  const container = document.getElementById('toast-container');
  const toast = document.createElement('div');
  toast.className = 'toast';
  toast.textContent = message;
  container.appendChild(toast);
  setTimeout(() => {
    toast.style.opacity = '0';
    toast.style.transition = `opacity ${getComputedStyle(document.documentElement).getPropertyValue('--duration-normal')}`;
    setTimeout(() => toast.remove(), 300);
  }, duration);
}

// ── function: prefersReducedMotion ──
/**
 * Check if prefers-reduced-motion is set.
 */
function prefersReducedMotion() {
  return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

// ── function: initModal ──
// === Modal Dialog Component ===

/**
 * Initialize the modal dialog with focus trapping and proper ARIA management.
 */
function initModal() {
  const openBtn = document.getElementById('open-modal-btn');
  const closeBtns = [document.getElementById('modal-cancel-btn'), document.getElementById('modal-confirm-btn')];
  const backdrop = document.getElementById('modal-backdrop');
  const dialog = document.getElementById('modal-dialog');

  let focusTrap = null;
  let triggerElement = null;

  function openModal(trigger) {
    triggerElement = trigger || openBtn;
    dialog.hidden = false;
    backdrop.hidden = false;
    // Make page behind inert by setting tabindex=-1 on main content temporarily
    document.getElementById('main-content').setAttribute('inert', '');

    focusTrap = createFocusTrap(dialog);
    focusTrap.activate();

    // Move focus into the dialog (first focusable element)
    requestAnimationFrame(() => {
      const firstFocusable = getFocusableElements(dialog)[0];
      if (firstFocusable) firstFocusable.focus();
    });
  }

  function closeModal() {
    dialog.hidden = true;
    backdrop.hidden = true;
    document.getElementById('main-content').removeAttribute('inert');
    focusTrap.deactivate();
    focusTrap = null;

    // Return focus to trigger
    if (triggerElement) triggerElement.focus();
  }

  openBtn.addEventListener('click', () => openModal(openBtn));

  closeBtns.forEach(btn => btn.addEventListener('click', closeModal));

  backdrop.addEventListener('click', closeModal);

  dialog.addEventListener('keydown', e => {
    if (e.key === 'Escape') { e.preventDefault(); closeModal(); }
  });

  // Confirm button action
  document.getElementById('modal-confirm-btn').addEventListener('click', () => {
    showToast('Action confirmed!');
    setTimeout(closeModal, 300);
  });
}

// ── function: initDropdown ──
// === Dropdown Menu Component ===

/**
 * Initialize dropdown menu with arrow-key navigation and proper ARIA management.
 */
function initDropdown() {
  const trigger = document.getElementById('dropdown-btn');
  const menu = document.getElementById('dropdown-menu-list');
  const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));

  let isOpen = false;
  let activeIndex = -1;

  function openMenu(focusFirst) {
    if (isOpen) return;
    isOpen = true;
    menu.hidden = false;
    trigger.setAttribute('aria-expanded', 'true');

    if (focusFirst) {
      activeIndex = 0;
      updateActiveItem();
    }
  }

  function closeMenu(returnFocus) {
    if (!isOpen) return;
    isOpen = false;
    menu.hidden = true;
    trigger.setAttribute('aria-expanded', 'false');
    items.forEach(item => item.removeAttribute('aria-selected'));
    activeIndex = -1;

    if (returnFocus !== false) trigger.focus();
  }

  function updateActiveItem() {
    items.forEach((item, i) => {
      if (i === activeIndex) {
        item.setAttribute('aria-selected', 'true');
        item.focus();
      } else {
        item.removeAttribute('aria-selected');
      }
    });
  }

  function selectItem(index) {
    const item = items[index];
    if (!item) return;
    const action = item.dataset.action || item.textContent.trim();
    showToast(`Selected: ${action}`);
    closeMenu(true);
  }

  // Trigger button events
  trigger.addEventListener('click', () => {
    isOpen ? closeMenu() : openMenu(false);
  });

  trigger.addEventListener('keydown', e => {
    if (e.key === 'ArrowDown' || e.key === 'Down') {
      e.preventDefault();
      openMenu(true);
    } else if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      isOpen ? closeMenu() : openMenu(false);
    }
  });

  // Menu keyboard navigation
  menu.addEventListener('keydown', e => {
    const key = e.key;
    if (key === 'ArrowDown' || key === 'Down') {
      e.preventDefault();
      activeIndex = (activeIndex + 1) % items.length;
      updateActiveItem();
    } else if (key === 'ArrowUp' || key === 'Up') {
      e.preventDefault();
      activeIndex = (activeIndex - 1 + items.length) % items.length;
      updateActiveItem();
    } else if (key === 'Home') {
      e.preventDefault();
      activeIndex = 0;
      updateActiveItem();
    } else if (key === 'End') {
      e.preventDefault();
      activeIndex = items.length - 1;
      updateActiveItem();
    } else if (key === 'Enter' || key === ' ') {
      e.preventDefault();
      selectItem(activeIndex);
    } else if (key === 'Escape') {
      e.preventDefault();
      closeMenu(true);
    } else if (key === 'Tab') {
      closeMenu(false); // Let Tab move focus naturally
    }
  });

  // Click on menu items
  items.forEach((item, index) => {
    item.addEventListener('click', () => selectItem(index));
    item.addEventListener('mouseenter', () => {
      activeIndex = index;
      updateActiveItem();
    });
  });

  // Close on outside click
  document.addEventListener('click', e => {
    if (isOpen && !menu.contains(e.target) && !trigger.contains(e.target)) {
      closeMenu(false);
    }
  });
}

// ── function: initTabs ──
// === Tabs Component ===

/**
 * Initialize tabs with roving tabindex and proper ARIA management.
 */
function initTabs() {
  const tablist = document.getElementById('tabs-demo');
  const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
  const panels = tabs.map(tab => document.getElementById(tab.getAttribute('aria-controls')));

  function activateTab(index, setFocus) {
    if (index < 0 || index >= tabs.length) return;

    // Deactivate all tabs and hide all panels
    tabs.forEach((tab, i) => {
      tab.setAttribute('aria-selected', 'false');
      tab.setAttribute('tabindex', '-1');
      tab.classList.remove('tab-active');
      if (panels[i]) panels[i].hidden = true;
    });

    // Activate selected tab and show its panel
    tabs[index].setAttribute('aria-selected', 'true');
    tabs[index].setAttribute('tabindex', '0');
    tabs[index].classList.add('tab-active');
    if (panels[index]) panels[index].hidden = false;

    if (setFocus) tabs[index].focus();
  }

  // Click handler for each tab
  tabs.forEach((tab, index) => {
    tab.addEventListener('click', () => activateTab(index, true));
  });

  // Keyboard navigation on the tablist
  tablist.addEventListener('keydown', e => {
    const currentIndex = tabs.indexOf(document.activeElement);
    if (currentIndex === -1) return;

    let newIndex = currentIndex;
    const key = e.key;

    if (key === 'ArrowLeft' || key === 'Left') {
      e.preventDefault();
      newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
    } else if (key === 'ArrowRight' || key === 'Right') {
      e.preventDefault();
      newIndex = (currentIndex + 1) % tabs.length;
    } else if (key === 'Home') {
      e.preventDefault();
      newIndex = 0;
    } else if (key === 'End') {
      e.preventDefault();
      newIndex = tabs.length - 1;
    } else {
      return;
    }

    activateTab(newIndex, true);
  });
}

// ── function: initCombobox ──
// === Combobox Component (aria-activedescendant pattern) ===

/**
 * Initialize combobox with filtering, arrow-key navigation via aria-activeddescendant.
 */
function initCombobox() {
  const input = document.getElementById('combobox-input');
  const listbox = document.getElementById('combobox-listbox');

  // ~20 programming language options
  const allOptions = [
    'JavaScript', 'TypeScript', 'Python', 'Rust', 'Go', 'Java', 'C++', 'C#',
    'Ruby', 'PHP', 'Swift', 'Kotlin', 'Scala', 'Elixir', 'Haskell', 'Lua',
    'Perl', 'Dart', 'Clojure', 'F#'
  ];

  let filteredOptions = [...allOptions];
  let activeIndex = -1;
  let isOpen = false;
  let originalValue = '';

  function renderListbox() {
    listbox.innerHTML = '';
    filteredOptions.forEach((option, i) => {
      const li = document.createElement('li');
      li.id = `combobox-option-${i}`;
      li.setAttribute('role', 'option');
      li.setAttribute('aria-selected', 'false');
      li.textContent = option;
      li.addEventListener('click', () => selectOption(i));
      listbox.appendChild(li);
    });

    if (activeIndex >= 0 && activeIndex < filteredOptions.length) {
      const activeEl = document.getElementById(`combobox-option-${activeIndex}`);
      if (activeEl) {
        activeEl.setAttribute('aria-selected', 'true');
        activeEl.scrollIntoView({ block: 'nearest' });
      }
    }

    input.setAttribute('aria-activedescendant',
      activeIndex >= 0 && activeIndex < filteredOptions.length ? `combobox-option-${activeIndex}` : '');
  }

  function openListbox() {
    if (filteredOptions.length === 0) return;
    isOpen = true;
    listbox.hidden = false;
    input.setAttribute('aria-expanded', 'true');
    activeIndex = -1;
    renderListbox();
  }

  function closeListbox() {
    isOpen = false;
    listbox.hidden = true;
    input.setAttribute('aria-expanded', 'false');
    input.removeAttribute('aria-activedescendant');
    activeIndex = -1;
  }

  function selectOption(index) {
    if (index < 0 || index >= filteredOptions.length) return;
    const selected = filteredOptions[index];
    input.value = selected;
    originalValue = selected;
    showToast(`Selected: ${selected}`);
    closeListbox();
  }

  function filterOptions(query) {
    const q = query.toLowerCase().trim();
    if (q === '') {
      filteredOptions = [...allOptions];
    } else {
      filteredOptions = allOptions.filter(opt => opt.toLowerCase().includes(q));
    }
    activeIndex = -1;
  }

  // Input events
  input.addEventListener('input', () => {
    filterOptions(input.value);
    if (isOpen) openListbox();
    else if (input.value.length > 0) openListbox();
  });

  input.addEventListener('focus', () => {
    originalValue = input.value;
    if (input.value.length > 0 && !isOpen) openListbox();
  });

  // Keyboard navigation
  input.addEventListener('keydown', e => {
    const key = e.key;

    if (key === 'ArrowDown' || key === 'Down') {
      e.preventDefault();
      if (!isOpen) { openListbox(); return; }
      activeIndex = Math.min(activeIndex + 1, filteredOptions.length - 1);
      renderListbox();
    } else if (key === 'ArrowUp' || key === 'Up') {
      e.preventDefault();
      if (!isOpen) return;
      activeIndex = Math.max(activeIndex - 1, 0);
      renderListbox();
    } else if (key === 'Enter') {
      e.preventDefault();
      if (isOpen && activeIndex >= 0) selectOption(activeIndex);
    } else if (key === 'Escape') {
      e.preventDefault();
      input.value = originalValue; // Revert to original value
      closeListbox();
    } else if (key === 'Home') {
      e.preventDefault();
      activeIndex = 0;
      renderListbox();
    } else if (key === 'End') {
      e.preventDefault();
      activeIndex = filteredOptions.length - 1;
      renderListbox();
    }
  });

  // Close on blur (focus out)
  input.addEventListener('blur', () => {
    setTimeout(() => {
      if (!listbox.contains(document.activeElement)) closeListbox();
    }, 100);
  });

  // Initialize with empty listbox
  renderListbox();
}

// ── function: initAccordion ──
// === Accordion / Disclosure Group Component ===

/**
 * Initialize accordion with animated panels and proper ARIA management.
 */
function initAccordion() {
  const group = document.getElementById('accordion-demo');
  const triggers = Array.from(group.querySelectorAll('.accordion-trigger'));

  triggers.forEach(trigger => {
    trigger.addEventListener('click', () => togglePanel(trigger));
    trigger.addEventListener('keydown', e => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        togglePanel(trigger);
      }
    });
  });

  function togglePanel(trigger) {
    const panelId = trigger.getAttribute('aria-controls');
    const panel = document.getElementById(panelId);
    if (!panel) return;

    const isExpanded = trigger.getAttribute('aria-expanded') === 'true';

    if (isExpanded) {
      // Collapse: animate height then set hidden
      collapsePanel(trigger, panel);
    } else {
      // Expand: remove hidden, measure, animate to full height
      expandPanel(trigger, panel);
    }
  }

  function expandPanel(trigger, panel) {
    trigger.setAttribute('aria-expanded', 'true');
    panel.hidden = false;

    if (prefersReducedMotion()) return;

    // Measure natural height
    const targetHeight = panel.scrollHeight;
    panel.style.height = '0px';
    panel.classList.add('animating');

    // Force reflow
    panel.offsetHeight;

    panel.style.height = `${targetHeight}px`;

    // After animation, remove constraints
    panel.addEventListener('transitionend', function onEnd() {
      panel.removeEventListener('transitionend', onEnd);
      panel.style.height = '';
      panel.classList.remove('animating');
    });

    // Safety timeout in case transitionend doesn't fire
    setTimeout(() => {
      if (panel.classList.contains('animating')) {
        panel.style.height = '';
        panel.classList.remove('animating');
      }
    }, 500);
  }

  function collapsePanel(trigger, panel) {
    trigger.setAttribute('aria-expanded', 'false');

    if (prefersReducedMotion()) {
      panel.hidden = true;
      return;
    }

    // Measure current height then animate to 0
    const startHeight = panel.scrollHeight;
    panel.style.height = `${startHeight}px`;
    panel.classList.add('animating');

    // Force reflow
    panel.offsetHeight;

    panel.style.height = '0px';

    panel.addEventListener('transitionend', function onEnd() {
      panel.removeEventListener('transitionend', onEnd);
      panel.hidden = true;
      panel.style.height = '';
      panel.classList.remove('animating');
    });

    // Safety timeout
    setTimeout(() => {
      if (panel.classList.contains('animating')) {
        panel.hidden = true;
        panel.style.height = '';
        panel.classList.remove('animating');
      }
    }, 500);
  }
}

// ── function: initScrollSpy ──
// === Scroll Spy — highlights current section in sidebar nav ===

/**
 * Initialize scroll spy to highlight the active section link in the sidebar.
 */
function initScrollSpy() {
  const sections = document.querySelectorAll('.component-section');
  const navLinks = Array.from(document.querySelectorAll('.sidebar-nav a'));

  function updateActiveLink() {
    let currentSection = '';
    for (const section of sections) {
      const rect = section.getBoundingClientRect();
      if (rect.top <= 120) {
        currentSection = section.id;
      }
    }

    navLinks.forEach(link => {
      const href = link.getAttribute('href');
      if (href === `#${currentSection}`) {
        link.style.color = 'var(--color-primary)';
        link.style.borderLeftColor = 'var(--color-primary)';
        link.style.fontWeight = '600';
      } else {
        link.style.color = '';
        link.style.borderLeftColor = '';
        link.style.fontWeight = '';
      }
    });
  }

  // Throttle scroll events
  let ticking = false;
  window.addEventListener('scroll', () => {
    if (!ticking) {
      requestAnimationFrame(() => { updateActiveLink(); ticking = false; });
      ticking = true;
    }
  }, { passive: true });

  // Initial call
  updateActiveLink();
}

// ── main ──
// === Main — Initialize all components on DOM ready ===

document.addEventListener('DOMContentLoaded', () => {
  initModal();
  initDropdown();
  initTabs();
  initCombobox();
  initAccordion();
  initScrollSpy();

  // Validate ARIA references (development check)
  validateAriaReferences();
});

/**
 * Verify all aria-controls and aria-labelledby references point to real elements.
 */
function validateAriaReferences() {
  const attrs = ['aria-controls', 'aria-labelledby', 'aria-owns', 'aria-describedby'];
  let errors = [];

  attrs.forEach(attr => {
    document.querySelectorAll(`[${attr}]`).forEach(el => {
      const ids = el.getAttribute(attr).split(' ').filter(Boolean);
      ids.forEach(id => {
        if (!document.getElementById(id)) {
          errors.push(`${el.tagName}#${el.id || '(no id)'} has ${attr}="${id}" but no element with that ID exists.`);
        }
      });
    });
  });

  if (errors.length > 0) {
    console.warn('ARIA reference validation failed:', errors);
  } else {
    console.log('✓ All ARIA references validated successfully.');
  }
}
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"65107199-8e68-4b2d-b0ba-871765c7f605","tokensIn":836112,"tokensOut":27610,"tokensTotal":863722,"turns":35,"toolCalls":34,"failedToolCalls":1,"timestamp":"2026-07-30T00:37:12.772Z"} -->