← Accessible Components results

Accessible Components

ornith 1.0 35b · typescript

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 60,406 bytes · SHA-256 0998fca96017
<!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 ── */
/* ===== Reset & Base Styles ===== */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --accent-primary: #2563eb;
  --accent-primary-hover: #1d4ed8;
  --accent-primary-light: #dbeafe;
  --text-primary: #1e293b;
  --text-secondary: #64748b;
  --text-muted: #94a3b8;
  --bg-body: #f8fafc;
  --bg-surface: #ffffff;
  --bg-hover: #f1f5f9;
  --bg-active: #e2e8f0;
  --border-color: #e2e8f0;
  --border-focus: #2563eb;
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
  --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
  --transition-fast: 150ms ease;
  --transition-normal: 250ms ease;
}

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font-sans);
  color: var(--text-primary);
  background: var(--bg-body);
  line-height: 1.6;
  min-height: 100vh;
}

/* ===== Focus Styles (never remove) ===== */
:focus-visible {
  outline: 3px solid var(--border-focus);
  outline-offset: 2px;
}

button:focus-visible,
a:focus-visible,
input:focus-visible,
[role="menuitem"]:focus-visible,
[role="tab"]:focus-visible,
[role="listbox"] [role="option"]:focus-visible {
  outline: 3px solid var(--border-focus);
  outline-offset: 2px;
}

/* ===== 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: 50%;
  transform: translateX(-50%);
  z-index: 10000;
  padding: 12px 24px;
  background: var(--accent-primary);
  color: #ffffff;
  font-weight: 600;
  text-decoration: none;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  transition: top var(--transition-fast);
}

.skip-link:focus {
  top: 0;
  outline: 3px solid var(--border-focus);
  outline-offset: -3px;
}

/* ===== Typography ===== */
h1, h2, h3, h4, h5, h6 {
  line-height: 1.2;
  font-weight: 700;
  color: var(--text-primary);
}

h2 {
  font-size: 1.75rem;
  margin-bottom: 0.5rem;
}

h3 {
  font-size: 1.25rem;
}

h4 {
  font-size: 1.1rem;
  margin-bottom: 0.75rem;
}

p {
  margin-bottom: 1rem;
  color: var(--text-secondary);
}

caption, th, td, dt, dd {
  text-align: left;
  color: var(--text-secondary);
}

code, pre {
  font-family: var(--font-mono);
  font-size: 0.875em;
}

/* ===== Utility Classes ===== */
.result-text {
  margin-top: 1rem;
  padding: 0.75rem 1rem;
  background: var(--bg-hover);
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  color: var(--text-secondary);
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0;
  font-size: 0.9rem;
}

th, td {
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border-color);
}

th {
  background: var(--bg-hover);
  font-weight: 600;
  color: var(--text-primary);
}

dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.5rem 1rem;
  margin: 1rem 0;
}

dt {
  font-weight: 600;
  color: var(--text-primary);
}

dd {
  color: var(--text-secondary);
}

ul {
  padding-left: 1.5rem;
  margin-bottom: 1rem;
}

li {
  margin-bottom: 0.25rem;
  color: var(--text-secondary);
}
/* ── css: layout ── */
/* ===== Layout: Sidebar + Main Content ===== */
.layout {
  display: grid;
  grid-template-columns: 260px 1fr;
  min-height: 100vh;
}

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  background: var(--bg-surface);
  border-right: 1px solid var(--border-color);
  padding: 2rem 1.5rem;
}

.sidebar-title {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin-bottom: 1rem;
  padding-left: 0.5rem;
}

.sidebar-nav {
  list-style: none;
  padding: 0;
  margin: 0;
}

.sidebar-nav li {
  margin-bottom: 2px;
}

.sidebar-nav a {
  display: block;
  padding: 0.5rem 0.75rem;
  color: var(--text-secondary);
  text-decoration: none;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  transition: background var(--transition-fast), color var(--transition-fast);
}

.sidebar-nav a:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
}

.sidebar-nav a:focus-visible {
  outline: 3px solid var(--border-focus);
  outline-offset: -2px;
}

.content {
  padding: 3rem 4rem;
  max-width: 900px;
}

.component-section {
  margin-bottom: 4rem;
  scroll-margin-top: 1rem;
}

.component-section h2 {
  margin-bottom: 0.5rem;
}

.caption {
  font-size: 0.875rem;
  color: var(--text-muted);
  margin-bottom: 1.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border-color);
}

.demo-area {
  background: var(--bg-surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  padding: 2rem;
  box-shadow: var(--shadow-sm);
}

/* ===== Responsive ===== */
@media (max-width: 768px) {
  .layout {
    grid-template-columns: 1fr;
  }

  .sidebar {
    position: relative;
    height: auto;
    border-right: none;
    border-bottom: 1px solid var(--border-color);
    padding: 1rem;
  }

  .content {
    padding: 2rem 1.5rem;
  }

  .component-section {
    margin-bottom: 3rem;
  }
}
/* ── css: components ── */
/* ===== Shared Component Styles ===== */

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.625rem 1.25rem;
  font-size: 0.9rem;
  font-weight: 500;
  line-height: 1.4;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
  font-family: inherit;
}

.btn-primary {
  background: var(--accent-primary);
  color: #ffffff;
  border-color: var(--accent-primary);
}

.btn-primary:hover {
  background: var(--accent-primary-hover);
  border-color: var(--accent-primary-hover);
}

.btn-primary:focus-visible {
  outline: 3px solid var(--border-focus);
  outline-offset: 2px;
}

.btn-secondary {
  background: var(--bg-surface);
  color: var(--text-primary);
  border-color: var(--border-color);
}

.btn-secondary:hover {
  background: var(--bg-hover);
  border-color: var(--text-muted);
}

.btn-secondary:focus-visible {
  outline: 3px solid var(--border-focus);
  outline-offset: 2px;
}

/* ===== Modal Dialog ===== */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.5);
  z-index: 999;
  animation: fadeIn var(--transition-normal);
}

.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--bg-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xl);
  z-index: 1000;
  padding: 2rem;
  max-width: 480px;
  width: calc(100% - 2rem);
  animation: slideUp var(--transition-normal);
}

.modal h3 {
  margin-bottom: 0.75rem;
  font-size: 1.25rem;
}

.modal p {
  margin-bottom: 1.5rem;
}

.modal-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
}

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

.dropdown-btn {
  min-width: 200px;
}

.dropdown-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  background: var(--bg-surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  list-style: none;
  padding: 0.25rem 0;
  min-width: 200px;
  z-index: 100;
}

.dropdown-item {
  display: block;
  width: 100%;
  padding: 0.5rem 1rem;
  border: none;
  background: none;
  text-align: left;
  font-size: 0.9rem;
  color: var(--text-primary);
  cursor: pointer;
  transition: background var(--transition-fast);
}

.dropdown-item:hover,
.dropdown-item[aria-selected="true"] {
  background: var(--accent-primary-light);
  color: var(--accent-primary);
}

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

.tab {
  padding: 0.75rem 1.25rem;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  transition: color var(--transition-fast), border-color var(--transition-fast);
  font-family: inherit;
}

.tab:hover {
  color: var(--text-primary);
}

.tab[aria-selected="true"] {
  color: var(--accent-primary);
  border-bottom-color: var(--accent-primary);
  font-weight: 600;
}

.tab:focus-visible {
  outline: 3px solid var(--border-focus);
  outline-offset: -2px;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

.tablist:focus-within .tab[aria-selected="true"] {
  outline: none;
}

.tabpanel {
  padding: 1.5rem 0;
  animation: fadeIn var(--transition-normal);
}

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

.combo-input {
  width: 100%;
  padding: 0.625rem 1rem;
  font-size: 0.9rem;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  color: var(--text-primary);
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
  font-family: inherit;
}

.combo-input:focus {
  outline: none;
  border-color: var(--accent-primary);
  box-shadow: 0 0 0 3px var(--accent-primary-light);
}

.combo-listbox {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  background: var(--bg-surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  list-style: none;
  padding: 0.25rem 0;
  max-height: 240px;
  overflow-y: auto;
  z-index: 100;
}

.combo-listbox [role="option"] {
  padding: 0.5rem 1rem;
  cursor: pointer;
  transition: background var(--transition-fast);
  font-size: 0.9rem;
}

.combo-listbox [role="option"][aria-selected="true"],
.combo-listbox [role="option"]:hover {
  background: var(--accent-primary-light);
  color: var(--accent-primary);
}

/* ===== Accordion ===== */
.accordion-wrapper {
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  overflow: hidden;
}

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

.accordion-header {
  margin: 0;
  font-size: 1rem;
}

.accordion-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 1rem 1.25rem;
  background: none;
  border: none;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--text-primary);
  cursor: pointer;
  text-align: left;
  transition: background var(--transition-fast);
  font-family: inherit;
}

.accordion-trigger:hover {
  background: var(--bg-hover);
}

.accordion-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  font-size: 1rem;
  color: var(--text-muted);
  transition: transform var(--transition-normal);
}

.accordion-trigger[aria-expanded="true"] .accordion-icon {
  transform: rotate(45deg);
}

.accordion-panel {
  overflow: hidden;
}

.accordion-panel-inner {
  padding: 0 1.25rem 1.25rem;
  color: var(--text-secondary);
  font-size: 0.9rem;
  line-height: 1.6;
}

/* ===== Animations ===== */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translate(-50%, -48%);
  }
  to {
    opacity: 1;
    transform: translate(-50%, -50%);
  }
}

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

  .modal {
    transform: translate(-50%, -50%);
  }
}
</style>
</head>
<body>
<div id="app">
  <a href="#main-content" class="skip-link">Skip to main content</a>

  <div class="layout">
    <nav class="sidebar" aria-label="Component navigation">
      <h2 class="sidebar-title">Components</h2>
      <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="content">
      <!-- Modal Dialog -->
      <section id="modal-section" class="component-section">
        <h2>Modal Dialog</h2>
        <p class="caption">Opens a dialog with focus trap · Tab cycles within · Esc closes · Click backdrop to dismiss</p>
        <div class="demo-area">
          <button id="open-modal-btn" class="btn btn-primary">Open Modal</button>
          <div id="modal-backdrop" class="modal-backdrop" hidden=""></div>
          <div id="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc" class="modal" hidden="">
            <h3 id="modal-title">Confirm Action</h3>
            <p id="modal-desc">Are you sure you want to proceed? This action cannot be undone.</p>
            <div class="modal-actions">
              <button id="modal-cancel-btn" class="btn btn-secondary">Cancel</button>
              <button id="modal-confirm-btn" class="btn btn-primary">Confirm</button>
            </div>
          </div>
        </div>
      </section>

      <!-- Dropdown Menu -->
      <section id="dropdown-section" class="component-section">
        <h2>Dropdown Menu</h2>
        <p class="caption">↑↓ navigate · Home/End first/last · Enter or Space select · Esc close · Tab closes and moves focus onward</p>
        <div class="demo-area">
          <div id="dropdown-container" class="dropdown-wrapper">
            <button id="dropdown-trigger" aria-haspopup="menu" aria-expanded="false" aria-controls="dropdown-listbox" class="btn btn-primary dropdown-btn">
              Choose an option ▾
            </button>
            <ul id="dropdown-listbox" role="menu" class="dropdown-menu" hidden="">
              <li role="none"><span role="menuitem" tabindex="-1" data-value="edit" class="dropdown-item">Edit</span></li>
              <li role="none"><span role="menuitem" tabindex="-1" data-value="duplicate" class="dropdown-item">Duplicate</span></li>
              <li role="none"><span role="menuitem" tabindex="-1" data-value="delete" class="dropdown-item">Delete</span></li>
              <li role="none"><span role="menuitem" tabindex="-1" data-value="rename" class="dropdown-item">Rename</span></li>
              <li role="none"><span role="menuitem" tabindex="-1" data-value="share" class="dropdown-item">Share</span></li>
            </ul>
          </div>
          <p id="dropdown-result" class="result-text">No selection yet</p>
        </div>
      </section>

      <!-- Tabs -->
      <section id="tabs-section" class="component-section">
        <h2>Tabs</h2>
        <p class="caption">←→ navigate tabs · Tab enters tablist → arrows switch · Enter/Space activates · Panels use hidden attribute</p>
        <div class="demo-area">
          <div id="tabs-container" class="tabs-wrapper">
            <div role="tablist" aria-label="File operations" class="tablist">
              <button role="tab" id="tab-overview" aria-selected="true" aria-controls="panel-overview" tabindex="0" class="tab active">Overview</button>
              <button role="tab" id="tab-details" aria-selected="false" aria-controls="panel-details" tabindex="-1" class="tab">Details</button>
              <button role="tab" id="tab-settings" aria-selected="false" aria-controls="panel-settings" tabindex="-1" class="tab">Settings</button>
            </div>
            <div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" class="tabpanel active">
              <h4>Project Overview</h4>
              <p>This is a showcase of accessible web components built with vanilla JavaScript. Each component follows WAI-ARIA Authoring Practices for keyboard interaction and screen reader compatibility.</p>
              <ul>
                <li>Modal Dialog — focus trap, inert page behind</li>
                <li>Dropdown Menu — arrow key navigation, wrapping</li>
                <li>Tabs — roving tabindex pattern</li>
                <li>Combobox — aria-activedescendant pattern</li>
                <li>Accordion — disclosure group with animation</li>
              </ul>
            </div>
            <div role="tabpanel" id="panel-details" aria-labelledby="tab-details" hidden="" class="tabpanel">
              <h4>Technical Details</h4>
              <p>Built with TypeScript, organized into modular components. No external dependencies — pure HTML, CSS, and JavaScript.</p>
              <table>
                <thead><tr><th>Feature</th><th>Status</th></tr></thead>
                <tbody>
                  <tr><td>Keyboard accessible</td><td>✓ Complete</td></tr>
                  <tr><td>Screen reader tested</td><td>✓ ARIA compliant</td></tr>
                  <tr><td>Reduced motion support</td><td>✓ Respected</td></tr>
                  <tr><td>200% zoom safe</td><td>✓ Fluid layout</td></tr>
                </tbody>
              </table>
            </div>
            <div role="tabpanel" id="panel-settings" aria-labelledby="tab-settings" hidden="" class="tabpanel">
              <h4>Configuration Options</h4>
              <p>All components support theming through CSS custom properties. Animation durations and colors are tunable constants at the top of the source.</p>
              <dl>
                <dt>Accent Color</dt><dd>Customizable via --accent-primary</dd>
                <dt>Animation Duration</dt><dd>Configurable in constants module</dd>
                <dt>Focus Style</dt><dd>High-contrast outline, never removed</dd>
              </dl>
            </div>
          </div>
        </div>
      </section>

      <!-- Combobox -->
      <section id="combobox-section" class="component-section">
        <h2>Combobox (Autocomplete)</h2>
        <p class="caption">Type to filter · ↓↑ highlight options · Enter selects · Esc closes and reverts · Focus out closes</p>
        <div class="demo-area">
          <div id="combobox-container" class="combobox-wrapper">
            <label for="combo-input" class="sr-only">Select a programming language</label>
            <input id="combo-input" role="combobox" aria-expanded="false" aria-controls="combo-listbox" aria-activedescendant="" autocomplete="off" placeholder="Type to search languages..." class="combo-input">
            <ul id="combo-listbox" role="listbox" class="combo-listbox" hidden="">
            </ul>
          </div>
          <p id="combo-result" class="result-text">No language selected</p>
        </div>
      </section>

      <!-- Accordion -->
      <section id="accordion-section" class="component-section">
        <h2>Accordion (Disclosure Group)</h2>
        <p class="caption">Enter or Space toggles sections · Multiple open at once · Animated height with hidden attribute</p>
        <div class="demo-area">
          <div id="accordion-container" class="accordion-wrapper" role="region" aria-label="Frequently asked questions">
            <div class="accordion-item">
              <h3 class="accordion-header">
                <button id="acc-btn-1" aria-expanded="false" aria-controls="acc-panel-1" class="accordion-trigger">
                  <span>What is this showcase?</span>
                  <span class="accordion-icon" aria-hidden="true">+</span>
                </button>
              </h3>
              <div id="acc-panel-1" role="region" aria-labelledby="acc-btn-1" hidden="" class="accordion-panel">
                <p>This is a demonstration of accessible web components built with vanilla JavaScript. Every widget follows WAI-ARIA Authoring Practices for keyboard interaction and screen reader compatibility.</p>
              </div>
            </div>
            <div class="accordion-item">
              <h3 class="accordion-header">
                <button id="acc-btn-2" aria-expanded="false" aria-controls="acc-panel-2" class="accordion-trigger">
                  <span>Do I need any dependencies?</span>
                  <span class="accordion-icon" aria-hidden="true">+</span>
                </button>
              </h3>
              <div id="acc-panel-2" role="region" aria-labelledby="acc-btn-2" hidden="" class="accordion-panel">
                <p>No! Everything is built with vanilla HTML, CSS, and JavaScript. There are no external libraries, frameworks, or CDN dependencies. The code is organized into TypeScript modules for clarity.</p>
              </div>
            </div>
            <div class="accordion-item">
              <h3 class="accordion-header">
                <button id="acc-btn-3" aria-expanded="false" aria-controls="acc-panel-3" class="accordion-trigger">
                  <span>How is keyboard navigation handled?</span>
                  <span class="accordion-icon" aria-hidden="true">+</span>
                </button>
              </h3>
              <div id="acc-panel-3" role="region" aria-labelledby="acc-btn-3" hidden="" class="accordion-panel">
                <p>Each component implements its own keyboard contract following WAI-ARIA patterns. Shared utilities handle focus trapping and aria-activedescendant management. Every interactive element has visible focus indicators.</p>
              </div>
            </div>
            <div class="accordion-item">
              <h3 class="accordion-header">
                <button id="acc-btn-4" aria-expanded="false" aria-controls="acc-panel-4" class="accordion-trigger">
                  <span>Does it support reduced motion?</span>
                  <span class="accordion-icon" aria-hidden="true">+</span>
                </button>
              </h3>
              <div id="acc-panel-4" role="region" aria-labelledby="acc-btn-4" hidden="" class="accordion-panel">
                <p>Yes. All animations respect the prefers-reduced-motion media query. When this preference is set, components transition instantly without any visual animation while maintaining full functionality.</p>
              </div>
            </div>
          </div>
        </div>
      </section>

      <!-- Live region for announcements -->
      <div id="live-announcer" aria-live="polite" aria-atomic="true" class="sr-only"></div>
    </main>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./components/modal.js":"src/components/modal.ts","./components/dropdown.js":"src/components/dropdown.ts","./components/tabs.js":"src/components/tabs.ts","./components/combobox.js":"src/components/combobox.ts","./components/accordion.js":"src/components/accordion.ts"},"src/components/modal.ts":{"../utils/focus-trap.js":"src/utils/focus-trap.ts","../utils/announce.js":"src/utils/announce.ts"},"src/components/dropdown.ts":{"../utils/announce.js":"src/utils/announce.ts"},"src/components/tabs.ts":{"../utils/announce.js":"src/utils/announce.ts"},"src/components/combobox.ts":{"../utils/announce.js":"src/utils/announce.ts"},"src/components/accordion.ts":{"../utils/announce.js":"src/utils/announce.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
/**
 * Entry Point — Accessible Component Showcase
 *
 * Initializes all component modules after DOM is ready.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const modal_js_1 = require("./components/modal.js");
const dropdown_js_1 = require("./components/dropdown.js");
const tabs_js_1 = require("./components/tabs.js");
const combobox_js_1 = require("./components/combobox.js");
const accordion_js_1 = require("./components/accordion.js");
function start() {
    // Initialize all components
    (0, modal_js_1.initModal)();
    (0, dropdown_js_1.initDropdown)();
    (0, tabs_js_1.initTabs)();
    (0, combobox_js_1.initCombobox)();
    (0, accordion_js_1.initAccordion)();
    // Smooth scroll for sidebar navigation links
    document.querySelectorAll('.sidebar-nav a').forEach((link) => {
        link.addEventListener('click', (event) => {
            const href = link.getAttribute('href');
            if (href?.startsWith('#')) {
                event.preventDefault();
                const target = document.querySelector(href);
                if (target instanceof HTMLElement) {
                    target.scrollIntoView({ behavior: 'smooth', block: 'start' });
                    // Update URL without jumping
                    history.pushState(null, '', href);
                    // Move focus to the section for accessibility
                    target.setAttribute('tabindex', '-1');
                    target.focus();
                }
            }
        });
    });
    console.log('Accessible Component Showcase initialized.');
}
// Boot on DOM ready
start();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
/**
 * Tunable design system constants.
 * Colors, spacing scale, animation durations — all centralized here.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.focus = exports.animation = exports.spacing = exports.theme = void 0;
exports.theme = {
    accent: {
        primary: '#2563eb',
        hover: '#1d4ed8',
        light: '#dbeafe',
    },
    text: {
        primary: '#1e293b',
        secondary: '#64748b',
        muted: '#94a3b8',
    },
    bg: {
        body: '#f8fafc',
        surface: '#ffffff',
        hover: '#f1f5f9',
        active: '#e2e8f0',
    },
    border: {
        default: '#e2e8f0',
        focus: '#2563eb',
    },
};
exports.spacing = {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px',
    xxl: '48px',
};
exports.animation = {
    fast: 150,
    normal: 250,
    slow: 400,
};
exports.focus = {
    width: '3px',
    offset: '2px',
    color: '#2563eb',
};
};

// ── module: src/utils/focus-trap.ts ──
__mods["src/utils/focus-trap.ts"] = function (exports, require, module) {
"use strict";
/**
 * Focus Trap Utility
 *
 * Traps keyboard focus within a container element.
 * Tab/Shift+Tab cycles through all focusable elements inside the trap.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFocusTrap = createFocusTrap;
const FOCUSABLE_SELECTOR = [
    'a[href]',
    'button:not([disabled])',
    'input:not([disabled]):not([type="hidden"])',
    'select:not([disabled])',
    'textarea:not([disabled])',
    '[tabindex]:not([tabindex="-1"])',
    '[contenteditable="true"]',
].join(', ');
/**
 * Creates a focus trap that cycles focus within `container`.
 * Returns a handle with activate/deactivate methods.
 */
function createFocusTrap(options) {
    const { container, onEscape, returnFocus = true } = options;
    let previouslyFocused = null;
    let isActive = false;
    function getFocusableElements() {
        const elements = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
        // Filter out hidden/disabled elements
        return elements.filter((el) => {
            if (el.offsetParent === null && !el.closest('[hidden]'))
                return false;
            if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true')
                return false;
            const style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden')
                return false;
            return true;
        });
    }
    function handleKeyDown(event) {
        if (!isActive)
            return;
        // Escape handled by caller via onEscape callback
        if (event.key === 'Escape') {
            event.stopPropagation();
            onEscape?.();
            return;
        }
        const focusable = getFocusableElements();
        if (focusable.length === 0)
            return;
        const firstEl = focusable[0];
        const lastEl = focusable[focusable.length - 1];
        const active = document.activeElement;
        if (event.key === 'Tab') {
            event.preventDefault();
            event.stopPropagation();
            if (event.shiftKey) {
                // Shift+Tab: if on first element, wrap to last
                if (active === firstEl || !container.contains(active)) {
                    lastEl.focus();
                }
                else {
                    const idx = focusable.indexOf(active);
                    if (idx > 0)
                        focusable[idx - 1].focus();
                    else
                        lastEl.focus();
                }
            }
            else {
                // Tab: if on last element, wrap to first
                if (active === lastEl || !container.contains(active)) {
                    firstEl.focus();
                }
                else {
                    const idx = focusable.indexOf(active);
                    if (idx < focusable.length - 1)
                        focusable[idx + 1].focus();
                    else
                        firstEl.focus();
                }
            }
        }
    }
    return {
        activate() {
            if (isActive)
                return;
            isActive = true;
            if (returnFocus) {
                previouslyFocused = document.activeElement;
            }
            container.setAttribute('data-focus-trap', 'active');
            document.addEventListener('keydown', handleKeyDown, true);
            // Focus the first focusable element
            const focusable = getFocusableElements();
            if (focusable.length > 0) {
                focusable[0].focus();
            }
            else {
                container.setAttribute('tabindex', '-1');
                container.focus();
            }
        },
        deactivate() {
            if (!isActive)
                return;
            isActive = false;
            container.removeAttribute('data-focus-trap');
            document.removeEventListener('keydown', handleKeyDown, true);
            // Return focus to the previously focused element
            if (returnFocus && previouslyFocused instanceof HTMLElement) {
                try {
                    previouslyFocused.focus();
                }
                catch {
                    // Element may have been removed from DOM
                }
            }
        },
    };
}
};

// ── module: src/utils/announce.ts ──
__mods["src/utils/announce.ts"] = function (exports, require, module) {
"use strict";
/**
 * Live Region Announcer
 *
 * Provides a helper to announce status messages to screen readers
 * via an aria-live region.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.announce = announce;
exports.announceAssertive = announceAssertive;
let announcer = null;
function getAnnouncer() {
    if (announcer)
        return announcer;
    announcer = document.getElementById('live-announcer');
    if (!announcer) {
        announcer = document.createElement('div');
        announcer.id = 'live-announcer';
        announcer.setAttribute('aria-live', 'polite');
        announcer.setAttribute('aria-atomic', 'true');
        announcer.className = 'sr-only';
        document.body.appendChild(announcer);
    }
    return announcer;
}
/**
 * Announce a message to screen readers.
 * Uses a timeout to ensure the live region is cleared before next announcement.
 */
function announce(message, priority = 'polite') {
    const el = getAnnouncer();
    // Clear first to ensure re-announcement of same text works
    el.textContent = '';
    el.setAttribute('aria-live', priority);
    requestAnimationFrame(() => {
        el.textContent = message;
    });
}
/**
 * Announce an important status change immediately.
 */
function announceAssertive(message) {
    announce(message, 'assertive');
}
};

// ── module: src/components/modal.ts ──
__mods["src/components/modal.ts"] = function (exports, require, module) {
"use strict";
/**
 * Modal Dialog Component
 *
 * Implements WAI-ARIA dialog pattern:
 * - role="dialog" with aria-modal="true"
 * - Focus trap within the dialog
 * - Focus returns to trigger on close
 * - Escape closes, backdrop click closes
 * - Page behind is inert while open
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initModal = initModal;
const focus_trap_js_1 = require("../utils/focus-trap.js");
const announce_js_1 = require("../utils/announce.js");
const TRIGGER_ID = 'open-modal-btn';
const DIALOG_ID = 'modal-dialog';
const BACKDROP_ID = 'modal-backdrop';
const CANCEL_ID = 'modal-cancel-btn';
const CONFIRM_ID = 'modal-confirm-btn';
let focusTrap = null;
function getTrigger() {
    return document.getElementById(TRIGGER_ID);
}
function getDialog() {
    return document.getElementById(DIALOG_ID);
}
function getBackdrop() {
    return document.getElementById(BACKDROP_ID);
}
function initModal() {
    const trigger = getTrigger();
    const dialog = getDialog();
    const backdrop = getBackdrop();
    // Set up focus trap
    focusTrap = (0, focus_trap_js_1.createFocusTrap)({
        container: dialog,
        onEscape: () => closeModal(),
    });
    function setInert() {
        // Set inert on everything outside the dialog
        const sidebar = document.querySelector('.sidebar');
        const skipLink = document.querySelector('.skip-link');
        if (sidebar instanceof HTMLElement)
            sidebar.setAttribute('inert', '');
        if (skipLink instanceof HTMLElement)
            skipLink.setAttribute('inert', '');
        // Set inert on all section elements EXCEPT the modal section
        const sections = document.querySelectorAll('.component-section');
        sections.forEach(section => {
            if (section.id !== 'modal-section') {
                section.setAttribute('inert', '');
            }
        });
    }
    function clearInert() {
        document.querySelectorAll('[inert]').forEach(el => el.removeAttribute('inert'));
    }
    function openModal() {
        // Show backdrop and dialog
        backdrop.removeAttribute('hidden');
        dialog.removeAttribute('hidden');
        // Make page behind inert (except the modal itself)
        setInert();
        // Activate focus trap (focuses first element inside dialog)
        if (focusTrap)
            focusTrap.activate();
        (0, announce_js_1.announce)('Dialog opened: Confirm Action');
    }
    function closeModal() {
        // Hide backdrop and dialog
        backdrop.setAttribute('hidden', '');
        dialog.setAttribute('hidden', '');
        // Clear inert from page
        clearInert();
        // Deactivate focus trap (returns focus to trigger)
        if (focusTrap)
            focusTrap.deactivate();
        (0, announce_js_1.announce)('Dialog closed');
    }
    function handleBackdropClick(event) {
        if (event.target === backdrop) {
            closeModal();
        }
    }
    trigger.addEventListener('click', openModal);
    backdrop.addEventListener('click', handleBackdropClick);
    document.getElementById(CANCEL_ID)?.addEventListener('click', closeModal);
    document.getElementById(CONFIRM_ID)?.addEventListener('click', () => {
        (0, announce_js_1.announce)('Action confirmed');
        closeModal();
    });
}
};

// ── module: src/components/dropdown.ts ──
__mods["src/components/dropdown.ts"] = function (exports, require, module) {
"use strict";
/**
 * Dropdown Menu Component
 *
 * Implements WAI-ARIA menu pattern:
 * - Button with aria-haspopup="menu" and aria-expanded
 * - Arrow key navigation (wrapping) within the menu
 * - Home/End to first/last item
 * - Escape closes, Tab closes naturally
 * - Enter/Space selects an item
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initDropdown = initDropdown;
const announce_js_1 = require("../utils/announce.js");
const TRIGGER_ID = 'dropdown-trigger';
const MENU_ID = 'dropdown-listbox';
const RESULT_ID = 'dropdown-result';
function getTrigger() {
    return document.getElementById(TRIGGER_ID);
}
function getMenu() {
    return document.getElementById(MENU_ID);
}
function getResult() {
    return document.getElementById(RESULT_ID);
}
function getItems() {
    const menu = getMenu();
    return Array.from(menu.querySelectorAll('[role="menuitem"]'));
}
function initDropdown() {
    const trigger = getTrigger();
    let state = { isOpen: false, activeIndex: -1 };
    function openMenu() {
        getMenu().removeAttribute('hidden');
        trigger.setAttribute('aria-expanded', 'true');
        state.isOpen = true;
        // Focus first item on keyboard open
        const items = getItems();
        if (items.length > 0) {
            setItemActive(items, 0);
            (0, announce_js_1.announce)(`Menu opened. ${items[0].textContent} selected.`);
        }
    }
    function closeMenu(returnFocus = true) {
        getMenu().setAttribute('hidden', '');
        trigger.setAttribute('aria-expanded', 'false');
        state.isOpen = false;
        state.activeIndex = -1;
        if (returnFocus) {
            trigger.focus();
        }
    }
    function toggleMenu() {
        if (state.isOpen) {
            closeMenu();
        }
        else {
            openMenu();
        }
    }
    function selectItem(item) {
        const value = item.dataset.value || item.textContent;
        getResult().textContent = `Selected: ${value}`;
        (0, announce_js_1.announce)(`${item.textContent} selected`);
        closeMenu(true);
    }
    function setItemActive(items, index) {
        // Remove active from all items
        items.forEach((item) => {
            item.removeAttribute('aria-selected');
            item.setAttribute('tabindex', '-1');
        });
        if (index >= 0 && index < items.length) {
            const target = items[index];
            target.setAttribute('aria-selected', 'true');
            target.setAttribute('tabindex', '0');
            state.activeIndex = index;
            // Scroll into view if needed
            target.scrollIntoView({ block: 'nearest' });
        }
    }
    function handleKeyDown(event) {
        const items = getItems();
        if (items.length === 0)
            return;
        switch (event.key) {
            case 'ArrowDown':
                event.preventDefault();
                if (!state.isOpen) {
                    openMenu();
                    setItemActive(items, 0);
                }
                else {
                    const nextIndex = state.activeIndex < items.length - 1
                        ? state.activeIndex + 1
                        : 0; // Wrap to first
                    setItemActive(items, nextIndex);
                    (0, announce_js_1.announce)(`${items[nextIndex].textContent} highlighted`);
                }
                break;
            case 'ArrowUp':
                event.preventDefault();
                if (state.isOpen) {
                    const prevIndex = state.activeIndex > 0
                        ? state.activeIndex - 1
                        : items.length - 1; // Wrap to last
                    setItemActive(items, prevIndex);
                    (0, announce_js_1.announce)(`${items[prevIndex].textContent} highlighted`);
                }
                break;
            case 'Home':
                event.preventDefault();
                if (state.isOpen) {
                    setItemActive(items, 0);
                    (0, announce_js_1.announce)(`${items[0].textContent} highlighted`);
                }
                break;
            case 'End':
                event.preventDefault();
                if (state.isOpen) {
                    setItemActive(items, items.length - 1);
                    (0, announce_js_1.announce)(`${items[items.length - 1].textContent} highlighted`);
                }
                break;
            case 'Enter':
            case ' ':
                event.preventDefault();
                if (state.isOpen && state.activeIndex >= 0) {
                    selectItem(items[state.activeIndex]);
                }
                break;
            case 'Escape':
                event.preventDefault();
                closeMenu(true);
                (0, announce_js_1.announce)('Menu closed');
                break;
            case 'Tab':
                // Tab naturally closes the menu and moves focus onward
                if (state.isOpen) {
                    closeMenu(false);
                }
                break;
        }
    }
    function handleItemClick(event) {
        const target = event.target;
        if (target.hasAttribute('role') && target.getAttribute('role') === 'menuitem') {
            selectItem(target);
        }
    }
    trigger.addEventListener('click', toggleMenu);
    trigger.addEventListener('keydown', handleKeyDown);
    getMenu().addEventListener('click', handleItemClick);
}
};

// ── module: src/components/tabs.ts ──
__mods["src/components/tabs.ts"] = function (exports, require, module) {
"use strict";
/**
 * Tabs Component
 *
 * Implements WAI-ARIA tabs pattern:
 * - role="tablist" with roving tabindex on tabs
 * - Arrow key navigation (wrapping) between tabs
 * - Each tab controls a panel via aria-controls/aria-labelledby
 * - Hidden panels use the hidden attribute
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initTabs = initTabs;
const announce_js_1 = require("../utils/announce.js");
const TABLIST_ID = 'tabs-container';
function getTablist() {
    return document.getElementById(TABLIST_ID);
}
function getTabs() {
    const tablist = getTablist();
    return Array.from(tablist.querySelectorAll('[role="tab"]'));
}
function getPanels() {
    const tablist = getTablist();
    const tabs = tablist.querySelectorAll('[role="tab"]');
    const panels = new Map();
    tabs.forEach((tab) => {
        const panelId = tab.getAttribute('aria-controls');
        if (panelId) {
            const panel = document.getElementById(panelId);
            if (panel) {
                panels.set(tab.id, panel);
            }
        }
    });
    return panels;
}
function initTabs() {
    let tabs = [];
    let activeIndex = 0;
    function refreshState() {
        tabs = getTabs();
        const currentIndex = tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true');
        if (currentIndex >= 0) {
            activeIndex = currentIndex;
        }
    }
    function activateTab(index, announceText) {
        refreshState();
        const panels = getPanels();
        // Update all tabs
        tabs.forEach((tab, i) => {
            const isActive = i === index;
            tab.setAttribute('aria-selected', String(isActive));
            tab.setAttribute('tabindex', isActive ? '0' : '-1');
            // Update corresponding panel
            const panelId = tab.getAttribute('aria-controls');
            if (panelId) {
                const panel = document.getElementById(panelId);
                if (panel) {
                    if (isActive) {
                        panel.removeAttribute('hidden');
                    }
                    else {
                        panel.setAttribute('hidden', '');
                    }
                }
            }
        });
        activeIndex = index;
        // Announce tab change
        const text = announceText || tabs[index].textContent?.trim() || 'Tab';
        (0, announce_js_1.announce)(`${text} tab selected`);
    }
    function handleKeyDown(event) {
        refreshState();
        if (tabs.length === 0)
            return;
        let newIndex = -1;
        switch (event.key) {
            case 'ArrowRight':
                event.preventDefault();
                newIndex = activeIndex < tabs.length - 1 ? activeIndex + 1 : 0; // Wrap
                break;
            case 'ArrowLeft':
                event.preventDefault();
                newIndex = activeIndex > 0 ? activeIndex - 1 : tabs.length - 1; // Wrap
                break;
            case 'Home':
                event.preventDefault();
                newIndex = 0;
                break;
            case 'End':
                event.preventDefault();
                newIndex = tabs.length - 1;
                break;
            default:
                return;
        }
        if (newIndex >= 0) {
            activateTab(newIndex);
            // Move focus to the new tab
            const newTab = document.getElementById(tabs[newIndex].id);
            if (newTab instanceof HTMLElement) {
                newTab.focus();
            }
        }
    }
    function handleTabClick(event) {
        const target = event.target;
        if (target.hasAttribute('role') && target.getAttribute('role') === 'tab') {
            const index = tabs.findIndex((t) => t.id === target.id);
            if (index >= 0) {
                activateTab(index, target.textContent?.trim());
            }
        }
    }
    // Initialize: ensure only first tab is active
    refreshState();
    if (tabs.length > 0 && activeIndex !== 0) {
        activateTab(0);
    }
    const tablist = getTablist();
    tablist.addEventListener('keydown', handleKeyDown);
    tablist.addEventListener('click', handleTabClick);
}
};

// ── module: src/components/combobox.ts ──
__mods["src/components/combobox.ts"] = function (exports, require, module) {
"use strict";
/**
 * Combobox (Autocomplete) Component
 *
 * Implements WAI-ARIA combobox pattern with aria-activedescendant:
 * - Input with role="combobox" and aria-controls/listbox
 * - Typing filters options in real-time
 * - Arrow keys move highlight via aria-activedescendant
 * - Enter selects, Escape closes (reverting value)
 * - Focus out closes the list
 * - Listbox is never keyboard-focusable itself
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initCombobox = initCombobox;
const announce_js_1 = require("../utils/announce.js");
const INPUT_ID = 'combo-input';
const LISTBOX_ID = 'combo-listbox';
const RESULT_ID = 'combo-result';
// ~20 programming language options
const ALL_OPTIONS = [
    'JavaScript', 'TypeScript', 'Python', 'Java', 'C++',
    'C#', 'Go', 'Rust', 'Swift', 'Kotlin',
    'Ruby', 'PHP', 'Scala', 'Haskell', 'Elixir',
    'Lua', 'Perl', 'R', 'MATLAB', 'Julia',
];
function getInput() {
    return document.getElementById(INPUT_ID);
}
function getListbox() {
    return document.getElementById(LISTBOX_ID);
}
function getResult() {
    return document.getElementById(RESULT_ID);
}
function initCombobox() {
    const input = getInput();
    let state = {
        isOpen: false,
        activeIndex: -1,
        filteredOptions: [...ALL_OPTIONS],
        originalValue: '',
    };
    function renderOptions(options) {
        const listbox = getListbox();
        listbox.innerHTML = '';
        options.forEach((option, index) => {
            const li = document.createElement('li');
            li.id = `combo-option-${index}`;
            li.setAttribute('role', 'option');
            li.setAttribute('aria-selected', String(index === state.activeIndex));
            li.textContent = option;
            listbox.appendChild(li);
        });
        // Update aria-activedescendant if open
        if (state.isOpen && options.length > 0) {
            const activeIdx = Math.max(0, state.activeIndex);
            input.setAttribute('aria-activedescendant', `combo-option-${activeIdx}`);
        }
        else {
            input.removeAttribute('aria-activedescendant');
        }
    }
    function filterOptions(query) {
        const lower = query.toLowerCase();
        state.filteredOptions = ALL_OPTIONS.filter((opt) => opt.toLowerCase().includes(lower));
    }
    function openListbox() {
        getListbox().removeAttribute('hidden');
        input.setAttribute('aria-expanded', 'true');
        state.isOpen = true;
        // Move highlight to first option if none active
        if (state.activeIndex < 0 && state.filteredOptions.length > 0) {
            state.activeIndex = 0;
        }
        renderOptions(state.filteredOptions);
        // Set aria-activedescendant after rendering
        if (state.isOpen && state.filteredOptions.length > 0 && state.activeIndex >= 0) {
            const optionId = `combo-option-${state.activeIndex}`;
            input.setAttribute('aria-activedescendant', optionId);
            // Scroll into view
            const el = document.getElementById(optionId);
            if (el)
                el.scrollIntoView({ block: 'nearest' });
            (0, announce_js_1.announce)(`${state.filteredOptions[state.activeIndex]} highlighted`);
        }
    }
    function closeListbox(restoreValue = false) {
        getListbox().setAttribute('hidden', '');
        input.setAttribute('aria-expanded', 'false');
        state.isOpen = false;
        state.activeIndex = -1;
        input.removeAttribute('aria-activedescendant');
        if (restoreValue) {
            input.value = state.originalValue;
        }
    }
    function selectOption(option) {
        input.value = option;
        getResult().textContent = `Selected: ${option}`;
        (0, announce_js_1.announce)(`${option} selected`);
        closeListbox(false);
    }
    function handleInput(event) {
        const target = event.target;
        state.originalValue = target.value;
        filterOptions(target.value);
        state.activeIndex = -1;
        if (target.value.length > 0) {
            openListbox();
        }
        else {
            closeListbox(false);
        }
    }
    function handleKeyDown(event) {
        const listbox = getListbox();
        const options = state.filteredOptions;
        switch (event.key) {
            case 'ArrowDown':
                event.preventDefault();
                if (!state.isOpen && options.length > 0) {
                    openListbox();
                    return;
                }
                if (state.isOpen && options.length > 0) {
                    const nextIndex = state.activeIndex < options.length - 1
                        ? state.activeIndex + 1
                        : 0; // Wrap
                    state.activeIndex = nextIndex;
                    const optionId = `combo-option-${nextIndex}`;
                    input.setAttribute('aria-activedescendant', optionId);
                    // Scroll into view
                    const el = document.getElementById(optionId);
                    if (el)
                        el.scrollIntoView({ block: 'nearest' });
                    (0, announce_js_1.announce)(`${options[nextIndex]} highlighted`);
                }
                break;
            case 'ArrowUp':
                event.preventDefault();
                if (state.isOpen && options.length > 0) {
                    const prevIndex = state.activeIndex > 0
                        ? state.activeIndex - 1
                        : options.length - 1; // Wrap
                    state.activeIndex = prevIndex;
                    const optionId = `combo-option-${prevIndex}`;
                    input.setAttribute('aria-activedescendant', optionId);
                    // Scroll into view
                    const el = document.getElementById(optionId);
                    if (el)
                        el.scrollIntoView({ block: 'nearest' });
                    (0, announce_js_1.announce)(`${options[prevIndex]} highlighted`);
                }
                break;
            case 'Enter':
                event.preventDefault();
                if (state.isOpen && state.activeIndex >= 0 && options[state.activeIndex]) {
                    selectOption(options[state.activeIndex]);
                }
                break;
            case 'Escape':
                event.preventDefault();
                closeListbox(true); // Revert value
                (0, announce_js_1.announce)('Closed without selection');
                break;
            case 'Tab':
                // If open and an option is highlighted, select it
                if (state.isOpen && state.activeIndex >= 0 && options[state.activeIndex]) {
                    selectOption(options[state.activeIndex]);
                }
                else if (state.isOpen) {
                    closeListbox(false);
                }
                break;
        }
    }
    function handleFocusOut() {
        // Close on focus out, but not when clicking an option
        setTimeout(() => {
            const active = document.activeElement;
            if (active && getListbox().contains(active))
                return;
            closeListbox(true);
        }, 0);
    }
    function handleOptionClick(event) {
        const target = event.target;
        if (target.hasAttribute('role') && target.getAttribute('role') === 'option') {
            const optionText = target.textContent || '';
            selectOption(optionText);
        }
    }
    // Initialize listbox with all options (hidden)
    filterOptions('');
    renderOptions(state.filteredOptions);
    input.addEventListener('input', handleInput);
    input.addEventListener('keydown', handleKeyDown);
    input.addEventListener('focusout', handleFocusOut);
    getListbox().addEventListener('click', handleOptionClick);
}
};

// ── module: src/components/accordion.ts ──
__mods["src/components/accordion.ts"] = function (exports, require, module) {
"use strict";
/**
 * Accordion (Disclosure Group) Component
 *
 * Implements WAI-ARIA disclosure pattern:
 * - Buttons with aria-expanded and aria-controls
 * - Enter/Space toggles sections
 * - Multiple sections can be open simultaneously
 * - Animated height transition, hidden attribute toggled at end
 * - Respects prefers-reduced-motion
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAccordion = initAccordion;
const announce_js_1 = require("../utils/announce.js");
const ACCORDION_ID = 'accordion-container';
function getContainer() {
    return document.getElementById(ACCORDION_ID);
}
function getItems() {
    const container = getContainer();
    return Array.from(container.querySelectorAll('.accordion-item'));
}
function isReducedMotion() {
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
function initAccordion() {
    const items = getItems();
    function toggleItem(item) {
        const trigger = item.querySelector('.accordion-trigger');
        if (!trigger)
            return;
        const panelId = trigger.getAttribute('aria-controls');
        let panel = null;
        if (panelId) {
            const el = document.getElementById(panelId);
            if (el instanceof HTMLElement)
                panel = el;
        }
        if (!panel)
            return;
        const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
        const willExpand = !isExpanded;
        // Update button state immediately
        trigger.setAttribute('aria-expanded', String(willExpand));
        // Get the content height by measuring visible children
        function getContentHeight(p) {
            const clone = p.cloneNode(true);
            clone.style.position = 'absolute';
            clone.style.visibility = 'hidden';
            clone.style.display = 'block';
            clone.removeAttribute('hidden');
            clone.style.height = '';
            clone.style.overflow = '';
            document.body.appendChild(clone);
            const height = clone.scrollHeight;
            document.body.removeChild(clone);
            return height;
        }
        if (willExpand) {
            // Expand: measure content, animate from 0 to full height
            panel.removeAttribute('hidden');
            const scrollHeight = getContentHeight(panel);
            panel.style.height = '0px';
            panel.style.overflow = 'hidden';
            if (isReducedMotion()) {
                panel.style.transition = 'none';
                panel.style.height = `${scrollHeight}px`;
                requestAnimationFrame(() => {
                    panel.style.transition = '';
                    panel.removeAttribute('style');
                    // Panel stays visible — do NOT set hidden on expand
                });
            }
            else {
                panel.style.transition = `height ${250}ms ease`;
                void panel.offsetHeight; // Force reflow
                panel.style.height = `${scrollHeight}px`;
                const onEnd = () => {
                    panel.removeEventListener('transitionend', onEnd);
                    panel.removeAttribute('style');
                    // Panel stays visible — do NOT set hidden on expand
                };
                panel.addEventListener('transitionend', onEnd);
            }
            (0, announce_js_1.announce)(`${trigger.querySelector('span')?.textContent} expanded`);
        }
        else {
            // Collapse: measure current height, animate to 0, then hide
            const currentHeight = getContentHeight(panel);
            panel.removeAttribute('hidden');
            panel.style.height = `${currentHeight}px`;
            panel.style.overflow = 'hidden';
            if (isReducedMotion()) {
                panel.style.transition = 'none';
                requestAnimationFrame(() => {
                    panel.style.transition = '';
                    panel.setAttribute('hidden', '');
                });
            }
            else {
                panel.style.transition = `height ${250}ms ease`;
                void panel.offsetHeight; // Force reflow
                panel.style.height = '0px';
                const onEnd = () => {
                    panel.removeEventListener('transitionend', onEnd);
                    panel.removeAttribute('style');
                    panel.setAttribute('hidden', '');
                };
                panel.addEventListener('transitionend', onEnd);
            }
            (0, announce_js_1.announce)(`${trigger.querySelector('span')?.textContent} collapsed`);
        }
    }
    function handleTriggerClick(event) {
        const target = event.target;
        if (target.closest('.accordion-trigger')) {
            const trigger = target.closest('.accordion-trigger');
            if (trigger) {
                const item = trigger.closest('.accordion-item');
                if (item)
                    toggleItem(item);
            }
        }
    }
    function handleKeyDown(event) {
        // Accordion buttons are naturally focusable, so they get Enter/Space natively.
        // No special keyboard handling needed beyond the click handler.
    }
    const container = getContainer();
    container.addEventListener('click', handleTriggerClick);
    container.addEventListener('keydown', handleKeyDown);
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ornith-1.0-35b","provider":"lmstudio","persona":"typescript","sessionId":"a99b1efa-0855-40c6-9173-0d66d577c96a","tokensIn":7648482,"tokensOut":60143,"tokensTotal":7708625,"turns":152,"toolCalls":151,"failedToolCalls":3,"timestamp":"2026-07-31T05:47:21.714Z"} -->