← Accessible Components results

Accessible Components

ox alpha · full

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 36,447 bytes · SHA-256 3932d86b0bef
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prism UI — Accessible Component Showcase</title>
<style>
/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
:root {
  /* palette */
  --accent-1: #4f46e5;        /* indigo */
  --accent-1-soft: #eef2ff;
  --accent-2: #0f766e;        /* teal */
  --accent-2-soft: #f0fdfa;
  --ink: #1e293b;
  --ink-muted: #64748b;
  --paper: #ffffff;
  --surface: #f8fafc;
  --border: #e2e8f0;
  --border-strong: #cbd5e1;
  --danger: #b91c1c;

  /* spacing scale */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;

  /* type scale */
  --fs-body: 1rem;
  --fs-sm: .875rem;
  --fs-lg: 1.125rem;

  /* radii & motion */
  --radius: 8px;
  --dur-fast: 120ms;
  --dur-med: 200ms;
  --ease: cubic-bezier(.33,.72,.35,1);
}

/* ============================================================
   BASE
   ============================================================ */
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--paper);
}
h1 { font-size: 1.75rem; letter-spacing: -.02em; margin: 0 0 var(--sp-2); }
h2 {
  font-size: 1.3rem; margin: 0 0 var(--sp-3); padding-top: var(--sp-6);
  border-top: 1px solid var(--border);
  scroll-margin-top: var(--sp-5);
}
h3 { font-size: var(--fs-body); margin: var(--sp-5) 0 var(--sp-2); }
p  { margin: 0 0 var(--sp-4); max-width: 62ch; }

/* focus indicator — never removed without replacement */
:focus-visible,
.modal.open :focus-visible {
  outline: 3px solid var(--accent-1);
  outline-offset: 2px;
  border-radius: 4px;
}

.skip-link {
  position: absolute; left: var(--sp-4); top: -100px; z-index: 100;
  background: var(--accent-1); color: #fff; padding: var(--sp-3) var(--sp-4);
  border-radius: 0 0 var(--radius) var(--radius); text-decoration: none;
  transition: top var(--dur-fast) var(--ease);
}
.skip-link:focus { top: 0; }

/* ============================================================
   LAYOUT
   ============================================================ */
.layout { display: grid; grid-template-columns: 240px minmax(0,1fr); gap: var(--sp-7); max-width: 1100px; margin: 0 auto; padding: var(--sp-6) var(--sp-5) var(--sp-8); }
.sidebar {
  position: sticky; top: var(--sp-6); align-self: start;
  border-right: 1px solid var(--border); padding-right: var(--sp-5);
}
.sidebar h2 { border: none; padding: 0; margin: 0 0 var(--sp-3); font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .08em; color: var(--ink-muted); }
.sidebar ul { list-style: none; margin: 0; padding: 0; }
.sidebar a {
  display: block; padding: var(--sp-2) var(--sp-3); margin-bottom: 2px;
  color: var(--ink-muted); text-decoration: none; border-radius: var(--radius);
  border-left: 3px solid transparent;
}
.sidebar a:hover { background: var(--surface); color: var(--ink); }
.sidebar a:focus-visible { outline-offset: -3px; }

.brand { display:flex; align-items:center; gap:var(--sp-3); margin-bottom: var(--sp-6); }
.logo {
  width: 36px; height: 36px; flex: none; border-radius: 10px;
  background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
  position: relative;
}
.logo::after { content:""; position:absolute; inset:10px; border-radius:50%; background:#fff; opacity:.9; }
.brand strong { display:block; line-height:1.2; }
.brand span { font-size: var(--fs-sm); color: var(--ink-muted); }

main > header { margin-bottom: var(--sp-6); }
.lede { color: var(--ink-muted); font-size: var(--fs-lg); }

/* demo cards */
.demo-card {
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden; margin-bottom: var(--sp-4);
}
.demo-stage { padding: var(--sp-6) var(--sp-5); background:
  linear-gradient(45deg, transparent 46%, var(--surface) 46%, var(--surface) 54%, transparent 54%) 0 0/28px 28px; }
.kbd-contract {
  display: block; padding: var(--sp-3) var(--sp-5);
  border-top: 1px solid var(--border); background: var(--surface);
  font-family: ui-monospace, Consolas, monospace; font-size: var(--fs-sm);
  color: var(--ink-muted);
}
kbd {
  font-family: inherit; background: #fff; border: 1px solid var(--border-strong);
  border-bottom-width: 2px; border-radius: 4px; padding: 0 5px; font-size: .85em;
}

/* shared control styles */
button, input {
  font: inherit; color: inherit;
}
.btn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4); border-radius: var(--radius);
  border: 1px solid var(--accent-1); background: var(--accent-1); color: #fff;
  cursor: pointer; transition: background var(--dur-fast), transform var(--dur-fast);
}
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: translateY(1px); }
.btn--ghost { background: #fff; color: var(--accent-1); }
.btn--teal { background: var(--accent-2); border-color: var(--accent-2); }
.caret { inline-size: .55em; border-inline: none; }

.field-label { display:block; font-weight:600; margin-bottom: var(--sp-2); }
.status-line { margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--ink-muted); min-height: 1.5em; }
.status-line strong { color: var(--accent-2); }

@media (max-width: 800px) {
  .layout { grid-template-columns: 1fr; gap: var(--sp-5); }
  .sidebar { position: static; border: none; padding: 0; }
  .sidebar ul { display:flex; flex-wrap: wrap; gap: var(--sp-2); }
  .sidebar a { border: 1px solid var(--border); }
}

/* ============================================================
   MODAL
   ============================================================ */
.backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgb(15 23 42 / .55);
  display: none;
}
.backdrop.open { display: flex; align-items: center; justify-content: center; padding: var(--sp-4); }
.modal {
  width: min(480px, 100%); background: var(--paper); border-radius: 12px;
  box-shadow: 0 24px 64px rgb(0 0 0 / .3);
  padding: var(--sp-6);
  animation: pop var(--dur-med) var(--ease);
}
@keyframes pop { from { transform: translateY(12px) scale(.97); opacity: 0; } }
.modal-head { display:flex; justify-content: space-between; align-items: start; gap: var(--sp-4); }
.modal h3 { margin: 0; font-size: var(--fs-lg); }
.icon-btn {
  border: 1px solid var(--border); background: #fff; border-radius: var(--radius);
  width: 34px; height: 34px; cursor: pointer; font-size: 1rem; line-height: 1;
}
.icon-btn:hover { background: var(--surface); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-3); margin-top: var(--sp-5); }

/* ============================================================
   DROPDOWN MENU
   ============================================================ */
.menu-wrap { position: relative; display: inline-block; }
.menu {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;
  min-width: 220px; background: #fff; border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: 0 12px 32px rgb(15 23 42 / .14);
  list-style: none; margin: 0; padding: var(--sp-2); display: none;
}
.menu.open { display: block; animation: fade var(--dur-fast) var(--ease); }
@keyframes fade { from { opacity: 0; transform: translateY(-4px); } }
.menu [role="menuitem"] {
  display: flex; align-items: center; gap: var(--sp-3);
  width: 100%; padding: var(--sp-2) var(--sp-3); border: none; background: none;
  border-radius: 6px; cursor: pointer; text-align: left;
}
.menu [role="menuitem"]:hover, .menu [role="menuitem"].active { background: var(--accent-1-soft); color: var(--accent-1); }
.menu .sep { height: 1px; background: var(--border); margin: var(--sp-2) calc(var(--sp-2) * -1); }
.menu .glyph { width: 1.2em; text-align: center; }

/* ============================================================
   TABS
   ============================================================ */
.tablist { display: flex; gap: var(--sp-1); border-bottom: 2px solid var(--border); }
[role="tab"] {
  padding: var(--sp-3) var(--sp-4); border: none; background: none; cursor: pointer;
  color: var(--ink-muted); border-bottom: 2px solid transparent; margin-bottom: -2px;
  font-weight: 600; transition: color var(--dur-fast);
}
[role="tab"]:hover { color: var(--ink); }
[role="tab"][aria-selected="true"] { color: var(--accent-1); border-bottom-color: var(--accent-1); }
[role="tabpanel"] { padding: var(--sp-5) var(--sp-2) 0; }
.plan-grid { display:grid; grid-template-columns: repeat(auto-fit,minmax(150px,1fr)); gap: var(--sp-4); margin-top: var(--sp-4); }
.plan { border: 1px solid var(--border); border-radius: var(--radius); padding: var(--sp-4); }
.plan b { color: var(--accent-1); font-size: var(--fs-lg); }
.changelog { padding-left: 1.2em; margin: 0; }
.changelog li { margin-bottom: var(--sp-2); }
.changelog time { color: var(--ink-muted); font-size: var(--fs-sm); display:block; }

/* ============================================================
   COMBOBOX
   ============================================================ */
.combobox-wrap { position: relative; max-width: 420px; }
.combobox-input {
  width: 100%; padding: var(--sp-3) var(--sp-4); border: 1px solid var(--border-strong);
  border-radius: var(--radius); font-size: var(--fs-body);
}
.combobox-input:focus-visible { outline-offset: 0; }
.listbox {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20;
  background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: 0 12px 32px rgb(15 23 42 / .14);
  list-style: none; margin: 0; padding: var(--sp-1); max-height: 240px; overflow-y: auto;
}
.listbox:not([hidden]) { display: block; }
.listbox [role="option"] { padding: var(--sp-2) var(--sp-3); border-radius: 6px; cursor: pointer; }
.listbox [role="option"][aria-selected="true"]::after { content:" ✓"; color: var(--accent-2); }
.listbox [role="option"].active { background: var(--accent-1); color: #fff; }
.listbox .no-results { padding: var(--sp-3); color: var(--ink-muted); font-style: italic; }

/* ============================================================
   ACCORDION
   ============================================================ */
.acc { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.acc-item + .acc-item { border-top: 1px solid var(--border); }
.acc-trigger {
  display: flex; justify-content: space-between; align-items: center; width: 100%;
  padding: var(--sp-4) var(--sp-5); border: none; background: #fff; cursor: pointer;
  font-weight: 600; font-size: var(--fs-body); text-align: left;
}
.acc-trigger:hover { background: var(--surface); }
.acc-trigger .chev { transition: transform var(--dur-med) var(--ease); color: var(--accent-1); }
.acc-trigger[aria-expanded="true"] .chev { transform: rotate(90deg); }
.acc-panel { overflow: hidden; transition: height var(--dur-med) var(--ease); }
.acc-panel-inner { padding: 0 var(--sp-5) var(--sp-5); color: var(--ink-muted); }
</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to main content</a>

<div class="layout">
  <!-- ============ SIDEBAR ============ -->
  <nav class="sidebar" aria-label="Components">
    <div class="brand">
      <div class="logo" aria-hidden="true"></div>
      <div><strong>Prism&nbsp;UI</strong><span>v2.4 · docs</span></div>
    </div>
    <h2 id="nav-heading">Components</h2>
    <ul aria-labelledby="nav-heading">
      <li><a href="#modal">Modal Dialog</a></li>
      <li><a href="#menu">Dropdown Menu</a></li>
      <li><a href="#tabs">Tabs</a></li>
      <li><a href="#combobox">Combobox</a></li>
      <li><a href="#accordion">Accordion</a></li>
    </ul>
  </nav>

  <!-- ============ MAIN ============ -->
  <main id="main" tabindex="-1">
    <header>
      <h1>Accessible Component Showcase</h1>
      <p class="lede">Five production-ready widgets built with vanilla JavaScript. Every component follows the WAI-ARIA Authoring Practices — try navigating the whole page with your keyboard only.</p>
    </header>

    <!-- ==================== 1. MODAL ==================== -->
    <section id="modal" aria-labelledby="modal-h">
      <h2 id="modal-h">Modal Dialog</h2>
      <p>A dialog that traps focus while open, hides the rest of the page from assistive tech via <code>inert</code>, and restores focus to its trigger on close.</p>
      <div class="demo-card">
        <div class="demo-stage">
          <button class="btn" id="modal-open-btn">Delete workspace…</button>
          <p class="status-line" id="modal-status" role="status"></p>
        </div>
        <span class="kbd-contract"><kbd>Tab</kbd>/<kbd>⇧Tab</kbd> cycle inside dialog · <kbd>Esc</kbd> close · click backdrop to dismiss · focus returns to trigger</span>
      </div>
    </section>

    <!-- ==================== 2. MENU ==================== -->
    <section id="menu" aria-labelledby="menu-h">
      <h2 id="menu-h">Dropdown Menu</h2>
      <p>A button-triggered menu with wrapping arrow-key navigation. Selection performs a visible action on the page.</p>
      <div class="demo-card">
        <div class="demo-stage">
          <div class="menu-wrap">
            <button class="btn btn--ghost" id="menu-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="theme-menu">
              Theme: System <span class="caret" aria-hidden="true">▾</span>
            </button>
            <ul class="menu" id="theme-menu" role="menu" aria-labelledby="menu-btn">
              <li role="none"><button role="menuitemradio" class="menu-item" data-value="Light" aria-checked="false" tabindex="-1"><span class="glyph" aria-hidden="true">☀️</span>Light</button></li>
              <li role="none"><button role="menuitemradio" class="menu-item" data-value="Dark" aria-checked="false" tabindex="-1"><span class="glyph" aria-hidden="true">🌙</span>Dark</button></li>
              <li role="none"><button role="menuitemradio" class="menu-item" data-value="System" aria-checked="true" tabindex="-1"><span class="glyph" aria-hidden="true">💻</span>System</button></li>
              <li role="none"><div class="sep" role="separator"></div></li>
              <li role="none"><button role="menuitem" class="menu-item" data-value="Customize…" tabindex="-1"><span class="glyph" aria-hidden="true">🎨</span>Customize…</button></li>
            </ul>
          </div>
          <p class="status-line" id="menu-status" role="status">Current theme: <strong>System</strong></p>
        </div>
        <span class="kbd-contract"><kbd>↓</kbd>/<kbd>↑</kbd> navigate (wraps) · <kbd>Home</kbd>/<kbd>End</kbd> jump · <kbd>Enter</kbd>/<kbd>Space</kbd> select · <kbd>Esc</kbd> close → focus on button · <kbd>Tab</kbd> closes menu</span>
      </div>
    </section>

    <!-- ==================== 3. TABS ==================== -->
    <section id="tabs" aria-labelledby="tabs-h">
      <h2 id="tabs-h">Tabs</h2>
      <p>Automatic activation: arrow keys move focus <em>and</em> switch panels. Only the active tab is in the tab order.</p>
      <div class="demo-card">
        <div class="demo-stage">
          <div class="tablist" role="tablist" aria-label="Product information">
            <button role="tab" id="tab-plans" aria-selected="true" aria-controls="panel-plans" tabindex="0">Pricing</button>
            <button role="tab" id="tab-changes" aria-selected="false" aria-controls="panel-changes" tabindex="-1">Changelog</button>
            <button role="tab" id="tab-faq" aria-selected="false" aria-controls="panel-faq" tabindex="-1">FAQ</button>
          </div>

          <div role="tabpanel" id="panel-plans" aria-labelledby="tab-plans" tabindex="0">
            <p>Simple, per-seat pricing. Every plan includes unlimited components and accessibility audits.</p>
            <div class="plan-grid">
              <div class="plan"><b>$0</b><br>Hobby — 3 projects</div>
              <div class="plan"><b>$19</b><br>Team — per seat / mo</div>
              <div class="plan"><b>Custom</b><br>Enterprise SSO &amp; SLA</div>
            </div>
          </div>
          <div role="tabpanel" id="panel-changes" aria-labelledby="tab-changes" tabindex="0" hidden>
            <ul class="changelog">
              <li><time datetime="2024-06-01">Jun 2024</time> Combobox now supports multi-select tokens.</li>
              <li><time datetime="2024-04-14">Apr 2024</time> Modal gains an <code>inert</code> backdrop strategy.</li>
              <li><time datetime="2024-02-02">Feb 2024</time> Tabs switched to automatic activation.</li>
            </ul>
          </div>
          <div role="tabpanel" id="panel-faq" aria-labelledby="tab-faq" tabindex="0" hidden>
            <p><strong>Is this screen-reader tested?</strong><br>Yes — NVDA, JAWS and VoiceOver are part of our CI checklist.</p>
            <p><label for="faq-email">Still stuck? Leave your email:</label><br>
            <input class="combobox-input" style="max-width:280px" type="email" id="faq-email" placeholder="you@example.com"></p>
          </div>
        </div>
        <span class="kbd-contract"><kbd>←</kbd>/<kbd>→</kbd> move between tabs (auto-activates) · <kbd>Home</kbd>/<kbd>End</kbd> first/last · one tab stop total; panel content is reachable by <kbd>Tab</kbd></span>
      </div>
    </section>

    <!-- ==================== 4. COMBOBOX ==================== -->
    <section id="combobox" aria-labelledby="combobox-h">
      <h2 id="combobox-h">Combobox</h2>
      <p>An autocomplete using the <code>aria-activedescendant</code> pattern — focus stays on the input while the highlight moves through the filtered list.</p>
      <div class="demo-card">
        <div class="demo-stage">
          <div class="combobox-wrap">
            <label class="field-label" id="combo-label" for="combo-input">Assign reviewer</label>
            <input class="combobox-input" type="text" id="combo-input" role="combobox"
                   aria-autocomplete="list" aria-expanded="false"
                   aria-controls="combo-listbox" aria-labelledby="combo-label"
                   autocomplete="off" placeholder="Start typing a name…">
            <ul class="listbox" id="combo-listbox" role="listbox" aria-labelledby="combo-label" hidden></ul>
          </div>
          <p class="status-line" id="combo-status" role="status"></p>
        </div>
        <span class="kbd-contract"><kbd>↓</kbd>/<kbd>↑</kbd> move highlight · <kbd>Enter</kbd> select · <kbd>Esc</kbd> close &amp; revert · focus out closes · list is never focused directly</span>
      </div>
    </section>

    <!-- ==================== 5. ACCORDION ==================== -->
    <section id="accordion" aria-labelledby="accordion-h">
      <h2 id="accordion-h">Accordion</h2>
      <p>Independent disclosure regions — any number can be open at once. Height animates; <code>hidden</code> toggles only after the exit animation.</p>
      <div class="demo-card">
        <div class="demo-stage">
          <div class="acc">
            <div class="acc-item">
              <h3 style="margin:0">
                <button class="acc-trigger" id="acc-t1" aria-expanded="false" aria-controls="acc-p1">
                  What licence applies? <span class="chev" aria-hidden="true">▶</span>
                </button>
              </h3>
              <div class="acc-panel" id="acc-p1" role="region" aria-labelledby="acc-t1" hidden>
                <div class="acc-panel-inner">All components ship under MIT. Use them commercially, no attribution required.</div>
              </div>
            </div>
            <div class="acc-item">
              <h3 style="margin:0">
                <button class="acc-trigger" id="acc-t2" aria-expanded="false" aria-controls="acc-p2">
                  Do you support RTL layouts? <span class="chev" aria-hidden="true">▶</span>
                </button>
              </h3>
              <div class="acc-panel" id="acc-p2" role="region" aria-labelledby="acc-t2" hidden>
                <div class="acc-panel-inner">Yes — every widget uses logical properties (<code>inline-size</code>, <code>margin-block</code>) so mirroring is automatic.</div>
              </div>
            </div>
            <div class="acc-item">
              <h3 style="margin:0">
                <button class="acc-trigger" id="acc-t3" aria-expanded="false" aria-controls="acc-p3">
                  How is this page animated respectfully? <span class="chev" aria-hidden="true">▶</span>
                </button>
              </h3>
              <div class="acc-panel" id="acc-p3" role="region" aria-labelledby="acc-t3" hidden>
                <div class="acc-panel-inner">All transitions are gated behind a <code>prefers-reduced-motion</code> media query and collapse instantly when motion is reduced.</div>
              </div>
            </div>
          </div>
        </div>
        <span class="kbd-contract"><kbd>Enter</kbd>/<kbd>Space</kbd> toggle · multiple panels may be open simultaneously · each trigger is its own tab stop</span>
      </div>
    </section>
  </main>
</div>
<!-- Modal markup lives outside the content column so the page behind can be made inert as siblings -->
<div class="backdrop" id="modal-backdrop" hidden>
  <div class="modal" id="confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
    <div class="modal-head">
      <h3 id="dialog-title">Delete workspace?</h3>
      <button class="icon-btn" id="modal-cancel" aria-label="Close dialog">✕</button>
    </div>
    <p id="dialog-desc" style="margin-top:var(--sp-3)">
      This permanently deletes <strong>acme-prod</strong> and all of its environments.
      This action cannot be undone.
    </p>
    <div class="modal-actions">
      <button class="btn btn--ghost" id="modal-cancel-2">Cancel</button>
      <button class="btn" style="background:var(--danger);border-color:var(--danger)" id="modal-confirm">Delete workspace</button>
    </div>
  </div>
</div>

<script>
/* ============================================================
   TUNABLE CONSTANTS (JS side)
   ============================================================ */
const REDUCED_MOTION = window.matchMedia('(prefers-reduced-motion: reduce)');
function animDuration(ms) { return REDUCED_MOTION.matches ? 0 : ms; }

/* ============================================================
   SHARED HELPERS
   ============================================================ */

/** Wrap-around index (accepts already-shifted index; handles negatives). */
function wrapIndex(i, len) {
  return ((i % len) + len) % len;
}

/**
 * Focus trap utility. Returns a controller { activate, deactivate }.
 * While active, Tab/Shift+Tab cycle within `container` only.
 */
function createFocusTrap(container) {
  const SELECTOR =
    'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
    'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

  function focusables() {
    return [...container.querySelectorAll(SELECTOR)]
      .filter(el => el.getClientRects().length && !el.closest('[inert]'));
  }

  function onKeydown(e) {
    if (e.key !== 'Tab') return;
    const items = focusables();
    if (!items.length) { e.preventDefault(); return; }
    const first = items[0], last = items[items.length - 1];
    if (e.shiftKey) {
      if (document.activeElement === first || !container.contains(document.activeElement)) {
        e.preventDefault(); last.focus();
      }
    } else {
      if (document.activeElement === last || !container.contains(document.activeElement)) {
        e.preventDefault(); first.focus();
      }
    }
  }

  return {
    activate() { document.addEventListener('keydown', onKeydown, true); },
    deactivate() { document.removeEventListener('keydown', onKeydown, true); },
  };
}

/* ============================================================
   1. MODAL DIALOG
   ============================================================ */
(function initModal() {
  const openBtn = document.getElementById('modal-open-btn');
  const backdrop = document.getElementById('modal-backdrop');
  const dialog = document.getElementById('confirm-dialog');
  const cancelBtn = document.getElementById('modal-cancel');
  const confirmBtn = document.getElementById('modal-confirm');
  const status = document.getElementById('modal-status');
  const trap = createFocusTrap(dialog);
  let lastFocused = null;

  function openModal() {
    lastFocused = (document.activeElement && document.activeElement !== document.body)
      ? document.activeElement : openBtn;
    // make the page behind inert
    [...document.body.children].forEach(el => {
      if (el !== backdrop && !el.contains(backdrop)) el.inert = true;
    });
    backdrop.hidden = false;
    backdrop.classList.add('open');
    trap.activate();
    cancelBtn.focus();
    document.addEventListener('keydown', onEsc);
  }

  function closeModal(message) {
    trap.deactivate();
    document.removeEventListener('keydown', onEsc);
    backdrop.classList.remove('open');
    backdrop.hidden = true;
    [...document.body.children].forEach(el => { el.inert = false; });
    status.textContent = message || '';
    (lastFocused || openBtn).focus();
  }

  function onEsc(e) {
    if (e.key === 'Escape' && backdrop.classList.contains('open')) {
      e.stopPropagation();
      closeModal();
    }
  }

  openBtn.addEventListener('click', openModal);
  cancelBtn.addEventListener('click', () => closeModal());
  document.getElementById('modal-cancel-2').addEventListener('click', () => closeModal());
  confirmBtn.addEventListener('click', () =>
    closeModal('Workspace “acme-prod” deleted.'));
  backdrop.addEventListener('mousedown', e => {
    if (!dialog.contains(e.target)) closeModal();
  });
})();

/* ============================================================
   2. DROPDOWN MENU
   ============================================================ */
(function initMenu() {
  const btn = document.getElementById('menu-btn');
  const menu = document.getElementById('theme-menu');
  const items = [...menu.querySelectorAll('[role="menuitem"], [role="menuitemradio"]')];
  const status = document.getElementById('menu-status');
  let isOpen = false;

  function setActive(i, focus = true) {
    items.forEach((it, j) => it.classList.toggle('active', i === j));
    if (focus) items[i].focus();
  }
  function currentIndex() {
    return Math.max(0, items.indexOf(document.activeElement));
  }

  function openMenu(focusFirst) {
    menu.classList.add('open');
    btn.setAttribute('aria-expanded', 'true');
    isOpen = true;
    if (focusFirst) setActive(0);
    else items.forEach(it => it.classList.remove('active'));
    document.addEventListener('keydown', onDocKeydown, true);
  }
  function closeMenu(refocus = true) {
    menu.classList.remove('open');
    btn.setAttribute('aria-expanded', 'false');
    isOpen = false;
    items.forEach(it => it.classList.remove('active'));
    document.removeEventListener('keydown', onDocKeydown, true);
    if (refocus) btn.focus();
  }

  function select(item) {
    if (item.getAttribute('role') === 'menuitemradio') {
      items.forEach(it => it.setAttribute('aria-checked', String(it === item)));
      status.innerHTML = 'Current theme: <strong></strong>';
      status.querySelector('strong').textContent = item.dataset.value;
      btn.childNodes[0].textContent = 'Theme: ' + item.dataset.value + ' ';
    } else {
      status.textContent = '“' + item.dataset.value + '” clicked.';
    }
    closeMenu();
  }

  function onMenuKeydown(e) {
    const i = currentIndex();
    switch (e.key) {
      case 'ArrowDown': e.preventDefault(); setActive(wrapIndex(i + 1, items.length)); break;
      case 'ArrowUp':   e.preventDefault(); setActive(wrapIndex(i - 1, items.length)); break;
      case 'Home':      e.preventDefault(); setActive(0); break;
      case 'End':       e.preventDefault(); setActive(items.length - 1); break;
      case 'Escape':    e.preventDefault(); closeMenu(); break;
      case 'Tab':       closeMenu(false); break; // let tab move naturally
    }
  }
  function onDocKeydown(e) {
    if (e.key === 'Escape' && isOpen) { e.preventDefault(); closeMenu(); }
  }

  btn.addEventListener('click', () => isOpen ? closeMenu(false) : openMenu(true));
  btn.addEventListener('keydown', e => {
    if (e.key === 'ArrowDown') { e.preventDefault(); openMenu(true); }
  });
  menu.addEventListener('keydown', onMenuKeydown);
  items.forEach(it => {
    it.addEventListener('click', () => select(it));
    it.addEventListener('mousemove', () => setActive(items.indexOf(it), false));
  });
  // click outside closes
  document.addEventListener('click', e => {
    if (isOpen && !menu.contains(e.target) && e.target !== btn) closeMenu(false);
  });
})();

/* ============================================================
   3. TABS
   ============================================================ */
(function initTabs() {
  const tablist = document.querySelector('[role="tablist"]');
  const tabs = [...tablist.querySelectorAll('[role="tab"]')];

  function activate(tab, setFocus = false) {
    tabs.forEach(t => {
      const selected = t === tab;
      t.setAttribute('aria-selected', String(selected));
      t.tabIndex = selected ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !selected;
    });
    if (setFocus) tab.focus();
  }

  tablist.addEventListener('keydown', e => {
    const i = tabs.indexOf(document.activeElement);
    if (i === -1) return;
    let handled = true;
    switch (e.key) {
      case 'ArrowRight': activate(tabs[wrapIndex(i + 1, tabs.length)], true); break;
      case 'ArrowLeft':  activate(tabs[wrapIndex(i - 1, tabs.length)], true); break;
      case 'Home':       activate(tabs[0], true); break;
      case 'End':        activate(tabs[tabs.length - 1], true); break;
      default: handled = false;
    }
    if (handled) e.preventDefault();
  });

  tabs.forEach((t, i) => {
    t.addEventListener('click', () => activate(t));
    t.addEventListener('focus', () => { if (t.getAttribute('aria-selected') !== 'true') activate(t); });
  });
})();

/* ============================================================
   4. COMBOBOX (aria-activedescendant pattern)
   ============================================================ */
(function initCombobox() {
  const OPTIONS = [
    'Ada Lovelace','Alan Turing','Barbara Liskov','Bjarne Stroustrup',
    'Donald Knuth','Edsger Dijkstra','Grace Hopper','Guido van Rossum',
    'James Gosling','Katherine Johnson','Ken Thompson','Linus Torvalds',
    'Margaret Hamilton','Niklaus Wirth','Radia Perlman','Rob Pike',
    'Tim Berners-Lee','Ursula Martin','Vint Cerf','Yukihiro Matsumoto'
  ];
  const input = document.getElementById('combo-input');
  const listbox = document.getElementById('combo-listbox');
  const statusEl = document.getElementById('combo-status');
  let visible = [];          // currently rendered option values
  let activeIdx = -1;

  function render(filter) {
    listbox.innerHTML = '';
    visible = OPTIONS.filter(o => o.toLowerCase().includes(filter.toLowerCase()));
    if (!visible.length) {
      const li = document.createElement('li');
      li.className = 'no-results';
      li.textContent = 'No matching reviewers';
      listbox.appendChild(li);
    } else {
      visible.forEach((val, i) => {
        const li = document.createElement('li');
        li.id = 'combo-opt-' + i;
        li.setAttribute('role', 'option');
        li.setAttribute('aria-selected', 'false');
        li.textContent = val;
        li.addEventListener('mousedown', e => { e.preventDefault(); choose(val); });
        listbox.appendChild(li);
      });
    }
    listbox.hidden = false;
    input.setAttribute('aria-expanded', 'true');
    activeIdx = -1;
    input.removeAttribute('aria-activedescendant');
  }

  function setActive(i, scroll = true) {
    const opts = [...listbox.querySelectorAll('[role="option"]')];
    if (!opts.length || i < -1 || i >= opts.length) return;
    opts.forEach(o => o.classList.remove('active'));
    activeIdx = i;
    if (i >= 0) {
      opts[i].classList.add('active');
      input.setAttribute('aria-activedescendant', opts[i].id);
      if (scroll) opts[i].scrollIntoView({ block: 'nearest' });
    } else {
      input.removeAttribute('aria-activedescendant');
    }
  }

  function announce(msg) { statusEl.textContent = msg; }

  function choose(val) {
    input.value = val;
    closeList();
    announce('Selected ' + val + '.');
    input.focus();
  }

  function closeList(revert = false) {
    if (revert) input.value = input.dataset.lastCommitted ?? '';
    listbox.hidden = true;
    input.setAttribute('aria-expanded', 'false');
    input.removeAttribute('aria-activedescendant');
    activeIdx = -1;
  }

  input.addEventListener('input', () => {
    announce('');
    render(input.value.trim());
  });

  input.addEventListener('keydown', e => {
    const open = !listbox.hidden;
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        if (!open) { render(input.value.trim()); setActive(0); }
        else setActive(Math.min(activeIdx + 1, visible.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        if (open) setActive(Math.max(activeIdx - 1, 0));
        break;
      case 'Home': if (open) { e.preventDefault(); setActive(0); } break;
      case 'End':  if (open) { e.preventDefault(); setActive(visible.length - 1); } break;
      case 'Enter':
        e.preventDefault();
        if (open && activeIdx >= 0 && visible[activeIdx]) choose(visible[activeIdx]);
        else if (open && visible.length === 1) choose(visible[0]);
        break;
      case 'Escape':
        if (open) { e.preventDefault(); closeList(true); announce('List closed, value reverted.'); }
        else { input.value = ''; announce('Cleared.'); }
        break;
    }
  });

  input.addEventListener('focusout', e => {
    if (!listbox.hidden && !listbox.contains(e.relatedTarget)) closeList();
  });

  // remember committed value when leaving field normally
  input.addEventListener('change', () => { input.dataset.lastCommitted = input.value; });
})();

/* ============================================================
   5. ACCORDION / DISCLOSURE
   ============================================================ */
(function initAccordion() {
  document.querySelectorAll('.acc-trigger').forEach(trigger => {
    const panel = document.getElementById(trigger.getAttribute('aria-controls'));

    function setOpen(open) {
      trigger.setAttribute('aria-expanded', String(open));
      if (open) {
        panel.hidden = false;
        const dur = animDuration(200);
        if (dur === 0) { panel.style.height = 'auto'; return; }
        panel.style.height = '0px';
        requestAnimationFrame(() => {
          panel.style.height = panel.scrollHeight + 'px';
        });
        panel.addEventListener('transitionend', function h(e) {
          if (e.propertyName !== 'height') return;
          panel.style.height = 'auto';
          panel.removeEventListener('transitionend', h);
        });
      } else {
        const dur = animDuration(200);
        if (dur === 0) { panel.hidden = true; return; }
        panel.style.height = panel.scrollHeight + 'px';
        requestAnimationFrame(() => {
          panel.style.transition = 'height ' + dur + 'ms var(--ease)';
          panel.style.height = '0px';
        });
        panel.addEventListener('transitionend', function h(e) {
          if (e.propertyName !== 'height') return;
          panel.hidden = true;
          panel.style.transition = '';
          panel.removeEventListener('transitionend', h);
        });
      }
    }

    trigger.addEventListener('click', () =>
      setOpen(trigger.getAttribute('aria-expanded') !== 'true'));
  });
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"7ee15bf3-da00-4e81-850a-5819cf40a00b","tokensIn":2364933,"tokensOut":25210,"tokensTotal":2390143,"cost":0,"turns":56,"toolCalls":57,"failedToolCalls":0,"timestamp":"2026-08-21T17:46:46.109Z"} -->