Accessible Components
ox alpha · full

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 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 & 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 & 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"} -->