Accessible Components
Qwen 3.6 27b · typescript

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 62,241 bytes · SHA-256 ecef25c5db67
<!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 ─────────────────────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
scroll-behavior: smooth;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
line-height: 1.6;
color: #1a1d23;
background: #ffffff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
font-size: 0.875em;
background: #f1f3f7;
padding: 0.15em 0.35em;
border-radius: 3px;
}
kbd {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.8em;
background: #f1f3f7;
border: 1px solid #d8dbe4;
border-bottom-width: 2px;
border-radius: 4px;
padding: 0.1em 0.4em;
white-space: nowrap;
}
/* ─── Skip Link ────────────────────────────────────────────────────────────── */
.a11y-skip-link {
position: absolute;
top: -100%;
left: 16px;
z-index: 2000;
background: #3b5bdb;
color: #ffffff;
padding: 10px 20px;
border-radius: 0 0 8px 8px;
font-weight: 600;
text-decoration: none;
font-size: 14px;
transition: top 0.15s ease;
}
.a11y-skip-link:focus {
top: 0;
outline: 3px solid #3b5bdb;
outline-offset: 2px;
}
/* ─── Screen Reader Only ──────────────────────────────────────────────────── */
.a11y-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;
}
/* ─── Focus Indicators ────────────────────────────────────────────────────── */
:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: 2px;
}
/* Remove outline from native focus for elements that use :focus-visible */
:focus:not(:focus-visible) {
outline: none;
}
/* ─── Layout ──────────────────────────────────────────────────────────────── */
#app {
display: flex;
min-height: 100vh;
}
.a11y-sidebar {
position: sticky;
top: 0;
width: 240px;
height: 100vh;
flex-shrink: 0;
background: #f8f9fb;
border-right: 1px solid #e2e5ec;
padding: 32px 0;
overflow-y: auto;
z-index: 100;
}
.a11y-sidebar nav ul {
list-style: none;
padding: 0 16px;
}
.a11y-sidebar nav li {
margin-bottom: 2px;
}
.a11y-sidebar nav a {
display: block;
padding: 8px 16px;
color: #5f6577;
text-decoration: none;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
border-left: 3px solid transparent;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.a11y-sidebar nav a:hover {
color: #1a1d23;
background: #eef0f5;
}
.a11y-sidebar nav a:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: -3px;
background: #e7ebf9;
border-left-color: #3b5bdb;
}
.a11y-sidebar nav a.active {
color: #3b5bdb;
background: #e7ebf9;
border-left-color: #3b5bdb;
}
.a11y-main {
flex: 1;
max-width: 860px;
padding: 48px 56px 80px;
min-width: 0;
}
/* ─── Header ──────────────────────────────────────────────────────────────── */
.a11y-header {
margin-bottom: 56px;
padding-bottom: 32px;
border-bottom: 1px solid #e2e5ec;
}
.a11y-header h1 {
font-size: 32px;
font-weight: 700;
color: #1a1d23;
letter-spacing: -0.02em;
margin-bottom: 12px;
line-height: 1.2;
}
.a11y-subtitle {
font-size: 16px;
color: #5f6577;
max-width: 600px;
line-height: 1.7;
}
/* ─── Sections ────────────────────────────────────────────────────────────── */
.a11y-section {
margin-bottom: 64px;
padding-top: 16px;
scroll-margin-top: 24px;
}
.a11y-section h2 {
font-size: 22px;
font-weight: 700;
color: #1a1d23;
margin-bottom: 8px;
letter-spacing: -0.01em;
}
.a11y-section-desc {
color: #5f6577;
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
}
.a11y-section-contract {
font-size: 12px;
color: #8b90a0;
margin-bottom: 20px;
font-family: 'SF Mono', 'Fira Code', monospace;
background: #f8f9fb;
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
border: 1px solid #e2e5ec;
}
.a11y-widget-container {
border: 1px solid #e2e5ec;
border-radius: 8px;
padding: 24px;
background: #ffffff;
min-height: 60px;
position: relative;
}
/* ─── Buttons ─────────────────────────────────────────────────────────────── */
.a11y-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
font-family: inherit;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
line-height: 1.4;
}
.a11y-btn:active {
transform: scale(0.98);
}
.a11y-btn-primary {
background: #3b5bdb;
color: #ffffff;
border-color: #3b5bdb;
}
.a11y-btn-primary:hover {
background: #3047c5;
border-color: #3047c5;
}
.a11y-btn-primary:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: 2px;
}
/* ─── Modal ───────────────────────────────────────────────────────────────── */
.a11y-modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.a11y-modal-dialog {
position: relative;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.1);
max-width: 520px;
width: 100%;
padding: 32px;
max-height: 85vh;
overflow-y: auto;
}
.a11y-modal-title {
font-size: 20px;
font-weight: 700;
color: #1a1d23;
margin-bottom: 12px;
line-height: 1.3;
}
.a11y-modal-close {
position: absolute;
top: 16px;
right: 16px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: #f1f3f7;
border: 1px solid #e2e5ec;
border-radius: 6px;
font-size: 20px;
line-height: 1;
cursor: pointer;
color: #5f6577;
transition: background 0.15s ease, color 0.15s ease;
font-family: inherit;
}
.a11y-modal-close:hover {
background: #e2e5ec;
color: #1a1d23;
}
.a11y-modal-close:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: 2px;
}
.a11y-modal-content {
color: #5f6577;
font-size: 14px;
line-height: 1.7;
margin-top: 8px;
}
/* ─── Dropdown Menu ───────────────────────────────────────────────────────── */
.a11y-menu-trigger {
position: relative;
}
.a11y-menu-list {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 200;
min-width: 180px;
background: #ffffff;
border: 1px solid #e2e5ec;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.06);
list-style: none;
padding: 6px;
}
.a11y-menu-item {
padding: 8px 12px;
font-size: 14px;
color: #1a1d23;
border-radius: 5px;
cursor: pointer;
transition: background 0.1s ease, color 0.1s ease;
white-space: nowrap;
}
.a11y-menu-item:hover,
.a11y-menu-item:focus-visible {
background: #e7ebf9;
color: #3b5bdb;
}
.a11y-menu-item:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: -3px;
}
.a11y-feedback {
margin-top: 12px;
padding: 8px 14px;
background: #edfae6;
color: #2b8a3e;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
display: none;
border: 1px solid #b4e6a3;
}
/* ─── Tabs ────────────────────────────────────────────────────────────────── */
.a11y-tablist {
display: flex;
gap: 0;
border-bottom: 2px solid #e2e5ec;
margin-bottom: 0;
}
.a11y-tab {
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
font-family: inherit;
background: none;
border: none;
color: #5f6577;
cursor: pointer;
position: relative;
transition: color 0.15s ease;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.a11y-tab:hover {
color: #1a1d23;
}
.a11y-tab[aria-selected="true"] {
color: #3b5bdb;
border-bottom-color: #3b5bdb;
font-weight: 600;
}
.a11y-tab:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: -3px;
}
.a11y-tab-panel {
padding: 20px 0 0;
color: #5f6577;
font-size: 14px;
line-height: 1.7;
}
.a11y-tab-panel p {
margin-bottom: 12px;
}
.a11y-tab-panel p:last-child {
margin-bottom: 0;
}
.a11y-tab-panel ul {
margin: 0 0 0 20px;
}
.a11y-tab-panel li {
margin-bottom: 6px;
}
.a11y-code-block {
background: #1a1d23;
color: #e2e5ec;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
font-size: 13px;
line-height: 1.6;
}
.a11y-code-block pre {
margin: 0;
font-family: 'SF Mono', 'Fira Code', monospace;
}
/* ─── Combobox ────────────────────────────────────────────────────────────── */
.a11y-combobox-input {
width: 100%;
max-width: 360px;
padding: 10px 14px;
font-size: 14px;
font-family: inherit;
border: 1px solid #d8dbe4;
border-radius: 6px;
background: #ffffff;
color: #1a1d23;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
line-height: 1.5;
}
.a11y-combobox-input::placeholder {
color: #8b90a0;
}
.a11y-combobox-input:focus {
outline: none;
border-color: #3b5bdb;
box-shadow: 0 0 0 3px rgba(59, 91, 219, 0.15);
}
.a11y-combobox-listbox {
position: absolute;
z-index: 200;
width: 100%;
max-width: 360px;
max-height: 200px;
overflow-y: auto;
background: #ffffff;
border: 1px solid #e2e5ec;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.06);
list-style: none;
padding: 6px;
margin-top: 4px;
}
.a11y-combobox-option {
padding: 8px 12px;
font-size: 14px;
color: #1a1d23;
border-radius: 5px;
cursor: pointer;
transition: background 0.1s ease;
}
.a11y-combobox-option:hover {
background: #f1f3f7;
}
.a11y-combobox-option[aria-selected="true"] {
background: #e7ebf9;
color: #3b5bdb;
font-weight: 500;
}
/* ─── Accordion ───────────────────────────────────────────────────────────── */
.a11y-accordion-button {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 16px 0;
font-size: 15px;
font-weight: 600;
font-family: inherit;
background: none;
border: none;
border-bottom: 1px solid #e2e5ec;
color: #1a1d23;
cursor: pointer;
text-align: left;
transition: color 0.15s ease;
}
.a11y-accordion-button:hover {
color: #3b5bdb;
}
.a11y-accordion-button:focus-visible {
outline: 3px solid #3b5bdb;
outline-offset: 2px;
border-radius: 4px;
}
.a11y-accordion-icon {
font-size: 12px;
transition: transform 0.2s ease;
flex-shrink: 0;
margin-left: 12px;
color: #8b90a0;
}
.a11y-accordion-button[aria-expanded="true"] .a11y-accordion-icon {
transform: rotate(90deg);
color: #3b5bdb;
}
.a11y-accordion-title {
flex: 1;
}
.a11y-accordion-region {
overflow: hidden;
padding: 0 0 16px;
}
.a11y-accordion-region p {
color: #5f6577;
font-size: 14px;
line-height: 1.7;
margin-bottom: 8px;
}
.a11y-accordion-region p:last-child {
margin-bottom: 0;
}
.a11y-accordion-region ul {
margin: 0 0 0 20px;
}
.a11y-accordion-region li {
color: #5f6577;
font-size: 14px;
line-height: 1.7;
margin-bottom: 6px;
}
/* ─── Reduced Motion ──────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
html {
scroll-behavior: auto;
}
}
/* ─── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.a11y-sidebar {
display: none;
}
.a11y-main {
padding: 32px 24px 64px;
}
.a11y-header h1 {
font-size: 26px;
}
}
</style>
</head>
<body>
<a class="a11y-skip-link" id="skip-link" href="#main-content">Skip to main content</a>
<aside class="a11y-sidebar" id="sidebar" aria-label="Component navigation">
<nav>
<ul>
<li><a href="#section-modal">Modal Dialog</a></li>
<li><a href="#section-menu">Dropdown Menu</a></li>
<li><a href="#section-tabs">Tabs</a></li>
<li><a href="#section-combobox">Combobox</a></li>
<li><a href="#section-accordion">Accordion</a></li>
</ul>
</nav>
</aside>
<main class="a11y-main" id="main-content">
<header class="a11y-header">
<h1>Accessible Component Showcase</h1>
<p class="a11y-subtitle">A fully keyboard- and screen-reader-accessible component library built with vanilla JavaScript, following WAI-ARIA Authoring Practices.</p>
</header>
<section class="a11y-section" id="section-modal" aria-labelledby="heading-modal">
<h2 id="heading-modal">Modal Dialog</h2>
<p class="a11y-section-desc">Opens a focus-trapped overlay. Press Escape or click the backdrop to close. Focus returns to the trigger on close.</p>
<div class="a11y-section-contract"><kbd>Enter</kbd> / <kbd>Space</kbd> open · <kbd>Esc</kbd> close · Tab trapped inside</div>
<div class="a11y-widget-container" id="widget-modal"></div>
</section>
<section class="a11y-section" id="section-menu" aria-labelledby="heading-menu">
<h2 id="heading-menu">Dropdown Menu</h2>
<p class="a11y-section-desc">A button-activated menu with full keyboard navigation. Arrow keys move between items with wrapping.</p>
<div class="a11y-section-contract"><kbd>Enter</kbd> / <kbd>Space</kbd> / <kbd>↓</kbd> open · <kbd>↑</kbd><kbd>↓</kbd> navigate · <kbd>Home</kbd> / <kbd>End</kbd> jump · <kbd>Esc</kbd> close</div>
<div class="a11y-widget-container" id="widget-menu"></div>
</section>
<section class="a11y-section" id="section-tabs" aria-labelledby="heading-tabs">
<h2 id="heading-tabs">Tabs</h2>
<p class="a11y-section-desc">Only the active tab is in the tab order. Arrow keys move between tabs. Hidden panels use the <code>hidden</code> attribute.</p>
<div class="a11y-section-contract"><kbd>←</kbd> / <kbd>→</kbd> switch tabs · <kbd>Home</kbd> / <kbd>End</kbd> jump · <kbd>Tab</kbd> enters panel</div>
<div class="a11y-widget-container" id="widget-tabs"></div>
</section>
<section class="a11y-section" id="section-combobox" aria-labelledby="heading-combobox">
<h2 id="heading-combobox">Combobox</h2>
<p class="a11y-section-desc">Type to filter a list of options. Arrow keys highlight items; Enter selects. Uses the aria-activedescendant pattern.</p>
<div class="a11y-section-contract">Type to filter · <kbd>↓</kbd> / <kbd>↑</kbd> highlight · <kbd>Enter</kbd> select · <kbd>Esc</kbd> close & revert</div>
<div class="a11y-widget-container" id="widget-combobox"></div>
</section>
<section class="a11y-section" id="section-accordion" aria-labelledby="heading-accordion">
<h2 id="heading-accordion">Accordion</h2>
<p class="a11y-section-desc">Expandable sections with animated open/close. Multiple sections may be open simultaneously.</p>
<div class="a11y-section-contract"><kbd>Enter</kbd> / <kbd>Space</kbd> toggle · multiple open allowed</div>
<div class="a11y-widget-container" id="widget-accordion"></div>
</section>
</main>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./modal":"src/modal.ts","./menu":"src/menu.ts","./tabs":"src/tabs.ts","./combobox":"src/combobox.ts","./accordion":"src/accordion.ts"},"src/modal.ts":{"./constants":"src/constants.ts","./helpers":"src/helpers.ts"},"src/menu.ts":{"./constants":"src/constants.ts","./helpers":"src/helpers.ts"},"src/tabs.ts":{"./helpers":"src/helpers.ts"},"src/combobox.ts":{"./constants":"src/constants.ts","./helpers":"src/helpers.ts"},"src/accordion.ts":{"./constants":"src/constants.ts","./helpers":"src/helpers.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 ──────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const modal_1 = require("./modal");
const menu_1 = require("./menu");
const tabs_1 = require("./tabs");
const combobox_1 = require("./combobox");
const accordion_1 = require("./accordion");
function start() {
// ─── Modal Dialog ──────────────────────────────────────────────────────
const modalSection = document.getElementById('section-modal');
const modalWidget = document.getElementById('widget-modal');
if (modalSection && modalWidget) {
const modal = new modal_1.ModalDialog(modalWidget, {
title: 'Confirmation Dialog',
content: 'This modal traps focus. Tab and Shift+Tab cycle within the dialog. Press Escape or click the backdrop to close. Focus returns to the trigger button on close.',
});
modalWidget.insertBefore(modal.getTrigger(), modalWidget.firstChild);
modal.mount();
}
// ─── Dropdown Menu ─────────────────────────────────────────────────────
const menuSection = document.getElementById('section-menu');
const menuWidget = document.getElementById('widget-menu');
if (menuSection && menuWidget) {
const actions = [];
const menuItems = [
{ label: 'Copy', action: () => actions.push('Copied to clipboard') },
{ label: 'Edit', action: () => actions.push('Editing selected item') },
{ label: 'Share', action: () => actions.push('Share dialog opened') },
{ label: 'Delete', action: () => actions.push('Item deleted') },
];
const menu = new menu_1.DropdownMenu(menuWidget, 'Actions ▾', menuItems);
menu.mount();
// Feedback display
const feedback = document.createElement('div');
feedback.className = 'a11y-feedback';
feedback.id = 'menu-feedback';
menuWidget.appendChild(feedback);
// Intercept actions to show feedback
const originalItems = [...menuItems];
originalItems.forEach((item, i) => {
const origAction = item.action;
item.action = () => {
origAction();
feedback.textContent = actions[actions.length - 1];
feedback.style.display = 'block';
setTimeout(() => { feedback.style.display = 'none'; }, 2000);
};
});
}
// ─── Tabs ──────────────────────────────────────────────────────────────
const tabsWidget = document.getElementById('widget-tabs');
if (tabsWidget) {
const tabItems = [
{
label: 'Overview',
content: `<p>This component demonstrates proper ARIA tab semantics. Only the active tab is in the tab order — arrow keys move between tabs. Tab panels use the <code>hidden</code> attribute, not just CSS hiding.</p>
<p>Try pressing <kbd>←</kbd> and <kbd>→</kbd> to navigate between tabs, or <kbd>Home</kbd> / <kbd>End</kbd> to jump to the first or last tab.</p>`,
},
{
label: 'Features',
content: `<ul>
<li>Single tab in tab order (roving tabindex pattern)</li>
<li>Arrow keys move between tabs with wrapping</li>
<li>Home/End jump to first/last tab</li>
<li>Hidden panels use the <code>hidden</code> attribute</li>
<li>Full keyboard and screen reader support</li>
</ul>`,
},
{
label: 'Code Example',
content: `<div class="a11y-code-block"><pre><code><div role="tablist">
<button role="tab"
aria-selected="true"
aria-controls="panel-1">Tab 1</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-2">Tab 2</button>
</div>
<div role="tabpanel" id="panel-1"
aria-labelledby="tab-1">...</div></code></pre></div>`,
},
];
const tabs = new tabs_1.Tabs(tabsWidget, tabItems);
tabs.mount();
}
// ─── Combobox ──────────────────────────────────────────────────────────
const comboboxWidget = document.getElementById('widget-combobox');
if (comboboxWidget) {
const countries = [
'Argentina', 'Australia', 'Austria', 'Belgium', 'Brazil',
'Canada', 'China', 'Colombia', 'Denmark', 'Egypt',
'Finland', 'France', 'Germany', 'Greece', 'India',
'Indonesia', 'Ireland', 'Italy', 'Japan', 'Kenya',
'Mexico', 'Netherlands', 'New Zealand', 'Norway', 'Pakistan',
'Poland', 'Portugal', 'Romania', 'Russia', 'Singapore',
'South Africa', 'South Korea', 'Spain', 'Sweden', 'Switzerland',
'Thailand', 'Turkey', 'United Kingdom', 'United States', 'Vietnam',
];
const combobox = new combobox_1.Combobox(comboboxWidget, 'Search countries...', countries);
combobox.mount();
}
// ─── Accordion ─────────────────────────────────────────────────────────
const accordionWidget = document.getElementById('widget-accordion');
if (accordionWidget) {
const accordionItems = [
{
title: 'What is this component?',
content: `<p>The accordion (also called a disclosure group) allows users to expand and collapse sections of content. Multiple sections can be open simultaneously.</p>`,
},
{
title: 'How does it work?',
content: `<p>Each section has a button with <code>aria-expanded</code> and <code>aria-controls</code> pointing to the associated region. The region uses <code>role="region"</code> and <code>aria-labelledby</code> referencing the button.</p>
<p>Open/close animations use CSS transitions on the <code>height</code> property. The <code>hidden</code> attribute is toggled only after the animation completes.</p>`,
},
{
title: 'Keyboard interaction',
content: `<p>Press <kbd>Enter</kbd> or <kbd>Space</kbd> on any accordion button to toggle its section. There are no special arrow-key behaviors since multiple sections may be open — standard tab order applies.</p>`,
},
{
title: 'Accessibility features',
content: `<ul>
<li>Proper <code>aria-expanded</code> state on each button</li>
<li><code>aria-controls</code> links each button to its region</li>
<li>Regions use <code>role="region"</code> with <code>aria-labelledby</code></li>
<li>Animations respect <code>prefers-reduced-motion</code></li>
<li>Content is never hidden during animation — <code>hidden</code> is set after transition ends</li>
</ul>`,
},
];
const accordion = new accordion_1.Accordion(accordionWidget, accordionItems);
accordion.mount();
}
}
// Auto-run
start();
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.SIDEBAR_WIDTH = exports.Z_INDEX = exports.BORDER_RADIUS = exports.FONT = exports.ANIMATION = exports.SPACING = exports.COLORS = void 0;
exports.COLORS = {
bg: '#ffffff',
bgSecondary: '#f8f9fb',
bgTertiary: '#f1f3f7',
text: '#1a1d23',
textSecondary: '#5f6577',
textTertiary: '#8b90a0',
border: '#e2e5ec',
borderFocus: '#3b5bdb',
accent: '#3b5bdb',
accentHover: '#3047c5',
accentLight: '#e7ebf9',
accentText: '#ffffff',
danger: '#e03131',
success: '#2b8a3e',
successLight: '#edfae6',
shadow: 'rgba(0, 0, 0, 0.12)',
shadowHeavy: 'rgba(0, 0, 0, 0.25)',
overlay: 'rgba(0, 0, 0, 0.45)',
focusRing: '#3b5bdb',
focusRingOffset: '2px',
};
exports.SPACING = {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px',
xxxl: '64px',
};
exports.ANIMATION = {
duration: '200ms',
durationSlow: '300ms',
easing: 'ease-out',
easingIn: 'ease-in',
};
exports.FONT = {
family: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
mono: "'JetBrains Mono', 'Fira Code', monospace",
size: {
xs: '12px',
sm: '13px',
md: '14px',
base: '15px',
lg: '18px',
xl: '22px',
xxl: '28px',
xxxl: '36px',
},
weight: {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
},
};
exports.BORDER_RADIUS = {
sm: '4px',
md: '6px',
lg: '8px',
xl: '12px',
};
exports.Z_INDEX = {
sidebar: 100,
dropdown: 200,
modal: 1000,
modalOverlay: 999,
skipLink: 2000,
};
exports.SIDEBAR_WIDTH = '240px';
};
// ── module: src/helpers.ts ──
__mods["src/helpers.ts"] = function (exports, require, module) {
"use strict";
// ─── Shared Accessibility Helpers ─────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActiveDescendantManager = void 0;
exports.createFocusTrap = createFocusTrap;
exports.debounce = debounce;
exports.prefersReducedMotion = prefersReducedMotion;
exports.setAria = setAria;
exports.genId = genId;
/**
* Focus-trap utility: keeps Tab/Shift+Tab cycling within a container.
* Returns a dispose function to remove the trap.
*/
function createFocusTrap(container) {
const focusableSelector = [
'a[href]:not([disabled]):not([tabindex="-1"])',
'button:not([disabled]):not([tabindex="-1"])',
'input:not([disabled]):not([tabindex="-1"])',
'select:not([disabled]):not([tabindex="-1"])',
'textarea:not([disabled]):not([tabindex="-1"])',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable="true"]',
].join(', ');
function getFocusableElements() {
return Array.from(container.querySelectorAll(focusableSelector)).filter((el) => !el.hasAttribute('disabled') && !isHidden(el));
}
function isHidden(el) {
return el.hidden || getComputedStyle(el).display === 'none' || getComputedStyle(el).visibility === 'hidden';
}
function handleKeydown(e) {
if (e.key !== 'Tab')
return;
const focusable = getFocusableElements();
if (focusable.length === 0) {
e.preventDefault();
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
// Shift+Tab: if on first element, wrap to last
if (document.activeElement === first || !container.contains(document.activeElement)) {
e.preventDefault();
last.focus();
}
}
else {
// Tab: if on last element, wrap to first
if (document.activeElement === last || !container.contains(document.activeElement)) {
e.preventDefault();
first.focus();
}
}
}
container.addEventListener('keydown', handleKeydown);
return () => container.removeEventListener('keydown', handleKeydown);
}
/**
* Manager for aria-activedescendant pattern.
* Tracks which descendant element is "active" (highlighted) without moving DOM focus.
*/
class ActiveDescendantManager {
constructor(host, container) {
this._activeId = null;
this.host = host;
this.container = container;
}
setActive(id) {
this._activeId = id;
this.host.setAttribute('aria-activedescendant', id ?? '');
if (id) {
const el = this.container.querySelector(`#${CSS.escape(id)}`);
if (el) {
el.setAttribute('aria-selected', 'true');
el.scrollIntoView({ block: 'nearest' });
}
}
// Clear previous selections
this.container.querySelectorAll('[aria-selected]').forEach((el) => {
if (el.id !== this._activeId) {
el.setAttribute('aria-selected', 'false');
}
});
}
get activeId() {
return this._activeId;
}
getItems() {
return Array.from(this.container.querySelectorAll('[role="option"]'));
}
moveHighlight(direction) {
const items = this.getItems();
if (items.length === 0)
return null;
const currentIndex = this._activeId
? items.findIndex((el) => el.id === this._activeId)
: direction === 'next'
? -1
: items.length;
let newIndex;
if (direction === 'next') {
newIndex = currentIndex + 1;
if (newIndex >= items.length)
newIndex = 0; // wrap
}
else {
newIndex = currentIndex - 1;
if (newIndex < 0)
newIndex = items.length - 1; // wrap
}
return items[newIndex]?.id ?? null;
}
reset() {
this.setActive(null);
}
}
exports.ActiveDescendantManager = ActiveDescendantManager;
/**
* Debounce function for filtering inputs.
*/
function debounce(fn, delay) {
let timer;
return ((...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
});
}
/**
* Check if prefers-reduced-motion is set.
*/
function prefersReducedMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
/**
* Safely set an attribute; omit value removes it.
*/
function setAria(el, attr, value) {
if (value === null || value === undefined) {
el.removeAttribute(attr);
}
else {
el.setAttribute(attr, typeof value === 'boolean' ? (value ? 'true' : 'false') : value);
}
}
/**
* Generate a unique ID prefix for this session.
*/
let idCounter = 0;
function genId(prefix = 'a11y') {
return `${prefix}-${++idCounter}`;
}
};
// ── module: src/modal.ts ──
__mods["src/modal.ts"] = function (exports, require, module) {
"use strict";
// ─── Modal Dialog Component ───────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModalDialog = void 0;
const constants_1 = require("./constants");
const helpers_1 = require("./helpers");
class ModalDialog {
constructor(container, options) {
this.disposeFocusTrap = null;
this.isOpen = false;
this._widgetContainer = null;
this.trigger = document.createElement('button');
this.trigger.className = 'a11y-btn a11y-btn-primary';
this.trigger.textContent = 'Open Modal Dialog';
this.trigger.setAttribute('aria-haspopup', 'dialog');
// Build overlay
this.overlay = document.createElement('div');
this.overlay.className = 'a11y-modal-overlay';
this.overlay.setAttribute('aria-hidden', 'true');
this.overlay.style.display = 'none';
// Build dialog
this.dialog = document.createElement('div');
this.dialog.className = 'a11y-modal-dialog';
this.dialog.setAttribute('role', 'dialog');
this.dialog.setAttribute('aria-modal', 'true');
const dialogId = (0, helpers_1.genId)('modal');
const titleId = (0, helpers_1.genId)('modal-title');
const contentId = (0, helpers_1.genId)('modal-content');
this.dialog.id = dialogId;
this.trigger.setAttribute('aria-controls', dialogId);
// Title
this.titleEl = document.createElement('h2');
this.titleEl.id = titleId;
this.titleEl.className = 'a11y-modal-title';
this.titleEl.textContent = options.title;
this.dialog.setAttribute('aria-labelledby', titleId);
// Close button
this.closeBtn = document.createElement('button');
this.closeBtn.className = 'a11y-modal-close';
this.closeBtn.setAttribute('aria-label', 'Close dialog');
this.closeBtn.textContent = '×';
// Content
this.contentEl = document.createElement('div');
this.contentEl.id = contentId;
this.contentEl.className = 'a11y-modal-content';
this.contentEl.textContent = options.content;
// Assemble
this.dialog.appendChild(this.titleEl);
this.dialog.appendChild(this.closeBtn);
this.dialog.appendChild(this.contentEl);
this.overlay.appendChild(this.dialog);
// Overlay is appended to body so it sits outside the inert content area
document.body.appendChild(this.overlay);
// Store reference to the widget container for inert management
this._widgetContainer = container;
}
mount() {
this.trigger.addEventListener('click', () => this.open());
this.closeBtn.addEventListener('click', () => this.close());
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay)
this.close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
e.preventDefault();
this.close();
}
});
}
open() {
if (this.isOpen)
return;
this.isOpen = true;
this.overlay.style.display = 'block';
this.overlay.setAttribute('aria-hidden', 'false');
this.dialog.setAttribute('aria-hidden', 'false');
(0, helpers_1.setAria)(this.trigger, 'aria-expanded', true);
// Inert the main content area (not body, so the overlay/dialog can receive focus)
const mainContent = document.getElementById('main-content');
if (mainContent)
mainContent.setAttribute('inert', '');
// Animate in
if (!(0, helpers_1.prefersReducedMotion)()) {
this.overlay.style.opacity = '0';
this.overlay.style.transition = `opacity ${constants_1.ANIMATION.duration} ${constants_1.ANIMATION.easing}`;
this.dialog.style.transform = 'scale(0.95) translateY(10px)';
this.dialog.style.transition = `transform ${constants_1.ANIMATION.duration} ${constants_1.ANIMATION.easing}`;
requestAnimationFrame(() => {
this.overlay.style.opacity = '1';
this.dialog.style.transform = 'scale(1) translateY(0)';
});
}
// Focus trap
this.disposeFocusTrap = (0, helpers_1.createFocusTrap)(this.dialog);
// Move focus into dialog
requestAnimationFrame(() => {
this.closeBtn.focus();
});
}
close() {
if (!this.isOpen)
return;
this.isOpen = false;
// Animate out
if (!(0, helpers_1.prefersReducedMotion)()) {
this.overlay.style.opacity = '0';
this.dialog.style.transform = 'scale(0.95) translateY(10px)';
}
else {
this.overlay.style.display = 'none';
}
const onTransitionEnd = () => {
this.overlay.style.display = 'none';
this.overlay.style.opacity = '';
this.dialog.style.transform = '';
this.overlay.removeEventListener('transitionend', onTransitionEnd);
};
if (!(0, helpers_1.prefersReducedMotion)()) {
this.overlay.addEventListener('transitionend', onTransitionEnd, { once: true });
// Fallback timeout
setTimeout(onTransitionEnd, 300);
}
// Remove focus trap
if (this.disposeFocusTrap) {
this.disposeFocusTrap();
this.disposeFocusTrap = null;
}
// Remove inert
const mainContent = document.getElementById('main-content');
if (mainContent)
mainContent.removeAttribute('inert');
(0, helpers_1.setAria)(this.trigger, 'aria-expanded', false);
// Return focus to trigger
this.trigger.focus();
}
getTrigger() {
return this.trigger;
}
}
exports.ModalDialog = ModalDialog;
};
// ── module: src/menu.ts ──
__mods["src/menu.ts"] = function (exports, require, module) {
"use strict";
// ─── Dropdown Menu Component ──────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.DropdownMenu = void 0;
const constants_1 = require("./constants");
const helpers_1 = require("./helpers");
class DropdownMenu {
constructor(container, buttonLabel, items) {
this.isOpen = false;
this.items = items;
// Button
this.button = document.createElement('button');
this.button.className = 'a11y-btn a11y-btn-primary a11y-menu-trigger';
this.button.textContent = buttonLabel;
this.button.setAttribute('aria-haspopup', 'menu');
this.button.setAttribute('aria-expanded', 'false');
// Menu
const menuId = (0, helpers_1.genId)('menu');
const statusId = (0, helpers_1.genId)('menu-status');
this.button.setAttribute('aria-controls', menuId);
this.menu = document.createElement('ul');
this.menu.className = 'a11y-menu-list';
this.menu.id = menuId;
this.menu.setAttribute('role', 'menu');
this.menu.style.display = 'none';
// Status region for screen readers
this.statusRegion = document.createElement('span');
this.statusRegion.id = statusId;
this.statusRegion.className = 'a11y-sr-only';
this.statusRegion.setAttribute('aria-live', 'polite');
this.statusRegion.setAttribute('aria-atomic', 'true');
// Menu items
items.forEach((item, index) => {
const li = document.createElement('li');
li.setAttribute('role', 'menuitem');
li.className = 'a11y-menu-item';
li.textContent = item.label;
li.tabIndex = 0;
li.id = `${menuId}-item-${index}`;
li.addEventListener('click', () => {
item.action();
this.close();
});
li.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
item.action();
this.close();
}
});
this.menu.appendChild(li);
});
container.appendChild(this.button);
container.appendChild(this.menu);
container.appendChild(this.statusRegion);
}
mount() {
this.button.addEventListener('click', () => this.toggle());
this.button.addEventListener('keydown', (e) => {
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.toggle();
break;
case 'ArrowDown':
e.preventDefault();
this.open();
this.focusFirstItem();
break;
case 'Escape':
if (this.isOpen) {
e.preventDefault();
this.close();
}
break;
}
});
// Menu key handling
this.menu.addEventListener('keydown', (e) => {
const menuItems = Array.from(this.menu.querySelectorAll('[role="menuitem"]'));
const currentIndex = menuItems.indexOf(document.activeElement);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (currentIndex < menuItems.length - 1) {
menuItems[currentIndex + 1].focus();
}
else {
menuItems[0].focus(); // wrap
}
break;
case 'ArrowUp':
e.preventDefault();
if (currentIndex > 0) {
menuItems[currentIndex - 1].focus();
}
else {
menuItems[menuItems.length - 1].focus(); // wrap
}
break;
case 'Home':
e.preventDefault();
menuItems[0]?.focus();
break;
case 'End':
e.preventDefault();
menuItems[menuItems.length - 1]?.focus();
break;
case 'Escape':
e.preventDefault();
this.close();
break;
case 'Tab':
this.close();
// Allow natural tab behavior
break;
}
});
// Close on outside click
document.addEventListener('click', (e) => {
if (this.isOpen && !this.button.contains(e.target) && !this.menu.contains(e.target)) {
this.close();
}
});
}
open() {
if (this.isOpen)
return;
this.isOpen = true;
this.menu.style.display = 'block';
(0, helpers_1.setAria)(this.button, 'aria-expanded', true);
if (!(0, helpers_1.prefersReducedMotion)()) {
this.menu.style.opacity = '0';
this.menu.style.transform = 'translateY(-4px)';
this.menu.style.transition = `opacity ${constants_1.ANIMATION.duration} ${constants_1.ANIMATION.easing}, transform ${constants_1.ANIMATION.duration} ${constants_1.ANIMATION.easing}`;
requestAnimationFrame(() => {
this.menu.style.opacity = '1';
this.menu.style.transform = 'translateY(0)';
});
}
this.statusRegion.textContent = `${this.items.length} items in menu`;
}
close() {
if (!this.isOpen)
return;
this.isOpen = false;
this.menu.style.display = 'none';
this.menu.style.opacity = '';
this.menu.style.transform = '';
(0, helpers_1.setAria)(this.button, 'aria-expanded', false);
this.button.focus();
}
toggle() {
if (this.isOpen) {
this.close();
}
else {
this.open();
}
}
focusFirstItem() {
const firstItem = this.menu.querySelector('[role="menuitem"]');
if (firstItem)
firstItem.focus();
}
}
exports.DropdownMenu = DropdownMenu;
};
// ── module: src/tabs.ts ──
__mods["src/tabs.ts"] = function (exports, require, module) {
"use strict";
// ─── Tabs Component ───────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tabs = void 0;
const helpers_1 = require("./helpers");
class Tabs {
constructor(container, tabItems) {
this.tabs = [];
this.panels = [];
this.activeIndex = 0;
const tablistId = (0, helpers_1.genId)('tablist');
// Tablist
this.tablist = document.createElement('div');
this.tablist.className = 'a11y-tablist';
this.tablist.setAttribute('role', 'tablist');
this.tablist.id = tablistId;
tabItems.forEach((item, index) => {
const tabId = (0, helpers_1.genId)('tab');
const panelId = (0, helpers_1.genId)('panel');
// Tab button
const tab = document.createElement('button');
tab.className = 'a11y-tab';
tab.setAttribute('role', 'tab');
tab.id = tabId;
tab.textContent = item.label;
tab.setAttribute('aria-controls', panelId);
tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
tab.tabIndex = index === 0 ? 0 : -1;
tab.addEventListener('click', () => this.activateTab(index));
this.tabs.push(tab);
this.tablist.appendChild(tab);
// Panel
const panel = document.createElement('div');
panel.className = 'a11y-tab-panel';
panel.setAttribute('role', 'tabpanel');
panel.id = panelId;
panel.setAttribute('aria-labelledby', tabId);
if (index !== 0) {
panel.hidden = true;
}
if (typeof item.content === 'string') {
panel.innerHTML = item.content;
}
else {
panel.appendChild(item.content);
}
this.panels.push(panel);
});
container.appendChild(this.tablist);
this.panels.forEach((panel) => container.appendChild(panel));
}
mount() {
this.tablist.addEventListener('keydown', (e) => {
const target = e.target;
if (!target.hasAttribute('role') || target.getAttribute('role') !== 'tab')
return;
const currentIndex = this.tabs.indexOf(target);
if (currentIndex === -1)
return;
let newIndex = currentIndex;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
newIndex = (currentIndex + 1) % this.tabs.length;
break;
case 'ArrowLeft':
e.preventDefault();
newIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = this.tabs.length - 1;
break;
default:
return;
}
this.activateTab(newIndex);
});
}
activateTab(index) {
if (index === this.activeIndex)
return;
// Deactivate current
this.tabs[this.activeIndex].setAttribute('aria-selected', 'false');
this.tabs[this.activeIndex].tabIndex = -1;
this.panels[this.activeIndex].hidden = true;
// Activate new
this.activeIndex = index;
this.tabs[index].setAttribute('aria-selected', 'true');
this.tabs[index].tabIndex = 0;
this.tabs[index].focus();
this.panels[index].hidden = false;
}
}
exports.Tabs = Tabs;
};
// ── module: src/combobox.ts ──
__mods["src/combobox.ts"] = function (exports, require, module) {
"use strict";
// ─── Combobox (Autocomplete) Component ────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.Combobox = void 0;
const constants_1 = require("./constants");
const helpers_1 = require("./helpers");
class Combobox {
constructor(container, placeholder, options) {
this.isOpen = false;
this.lastValue = '';
this.options = options;
this.filteredOptions = [...options];
const inputId = (0, helpers_1.genId)('combobox-input');
const listboxId = (0, helpers_1.genId)('combobox-listbox');
// Input
this.input = document.createElement('input');
this.input.type = 'text';
this.input.className = 'a11y-combobox-input';
this.input.id = inputId;
this.input.setAttribute('role', 'combobox');
this.input.setAttribute('aria-autocomplete', 'list');
this.input.setAttribute('aria-controls', listboxId);
this.input.setAttribute('aria-expanded', 'false');
this.input.setAttribute('aria-haspopup', 'listbox');
this.input.setAttribute('aria-activedescendant', '');
this.input.setAttribute('autocomplete', 'off');
this.input.placeholder = placeholder;
// Listbox
this.listbox = document.createElement('ul');
this.listbox.className = 'a11y-combobox-listbox';
this.listbox.id = listboxId;
this.listbox.setAttribute('role', 'listbox');
this.listbox.setAttribute('aria-label', 'Suggestions');
this.listbox.style.display = 'none';
// Manager for aria-activedescendant
this.manager = new helpers_1.ActiveDescendantManager(this.input, this.listbox);
container.appendChild(this.input);
container.appendChild(this.listbox);
}
mount() {
// Filter on input
this.input.addEventListener('input', () => {
const value = this.input.value;
this.lastValue = value;
this.filterAndRender(value);
});
// Keyboard handling
this.input.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!this.isOpen) {
this.open();
}
this.manager.setActive(this.manager.moveHighlight('next'));
break;
case 'ArrowUp':
e.preventDefault();
this.manager.setActive(this.manager.moveHighlight('prev'));
break;
case 'Enter':
e.preventDefault();
if (this.isOpen && this.manager.activeId) {
this.selectActive();
}
break;
case 'Escape':
e.preventDefault();
this.close();
this.input.value = this.lastValue;
this.filterAndRender(this.lastValue);
break;
case 'Tab':
this.close();
break;
}
});
// Close on focus out
this.input.addEventListener('blur', () => {
// Small delay to allow click on option to register
setTimeout(() => {
if (this.isOpen) {
this.close();
}
}, 100);
});
// Click on options
this.listbox.addEventListener('click', (e) => {
const option = e.target.closest('[role="option"]');
if (option) {
this.selectOption(option.textContent ?? '');
}
});
}
filterAndRender(query) {
const lower = query.toLowerCase();
this.filteredOptions = this.options.filter((opt) => opt.toLowerCase().includes(lower));
this.renderOptions();
if (this.filteredOptions.length > 0 && query.length > 0) {
if (!this.isOpen)
this.open();
}
else if (this.filteredOptions.length === 0) {
this.close();
}
}
renderOptions() {
this.listbox.innerHTML = '';
this.manager.reset();
this.filteredOptions.forEach((opt) => {
const li = document.createElement('li');
li.setAttribute('role', 'option');
li.className = 'a11y-combobox-option';
li.id = (0, helpers_1.genId)('combobox-option');
li.textContent = opt;
li.setAttribute('aria-selected', 'false');
li.addEventListener('mouseenter', () => {
this.manager.setActive(li.id);
});
this.listbox.appendChild(li);
});
}
open() {
if (this.isOpen)
return;
this.isOpen = true;
this.listbox.style.display = 'block';
(0, helpers_1.setAria)(this.input, 'aria-expanded', true);
if (!(0, helpers_1.prefersReducedMotion)()) {
this.listbox.style.opacity = '0';
this.listbox.style.transform = 'translateY(-4px)';
this.listbox.style.transition = `opacity ${constants_1.ANIMATION.duration} ${constants_1.ANIMATION.easing}, transform ${constants_1.ANIMATION.duration} ${constants_1.ANIMATION.easing}`;
requestAnimationFrame(() => {
this.listbox.style.opacity = '1';
this.listbox.style.transform = 'translateY(0)';
});
}
}
close() {
if (!this.isOpen)
return;
this.isOpen = false;
this.listbox.style.display = 'none';
this.listbox.style.opacity = '';
this.listbox.style.transform = '';
(0, helpers_1.setAria)(this.input, 'aria-expanded', false);
this.manager.reset();
}
selectActive() {
const activeId = this.manager.activeId;
if (!activeId)
return;
const option = this.listbox.querySelector(`#${CSS.escape(activeId)}`);
if (option) {
this.selectOption(option.textContent ?? '');
}
}
selectOption(value) {
this.input.value = value;
this.lastValue = value;
this.close();
this.input.focus();
}
}
exports.Combobox = Combobox;
};
// ── module: src/accordion.ts ──
__mods["src/accordion.ts"] = function (exports, require, module) {
"use strict";
// ─── Accordion / Disclosure Group Component ───────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.Accordion = void 0;
const constants_1 = require("./constants");
const helpers_1 = require("./helpers");
class Accordion {
constructor(container, items) {
this.items = [];
items.forEach((item, index) => {
const buttonId = (0, helpers_1.genId)('accordion-btn');
const regionId = (0, helpers_1.genId)('accordion-region');
// Button
const button = document.createElement('button');
button.className = 'a11y-accordion-button';
button.id = buttonId;
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-controls', regionId);
button.setAttribute('aria-label', `${item.title}${index === 0 ? '' : ' (collapsed)'}`);
const icon = document.createElement('span');
icon.className = 'a11y-accordion-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = '▸';
const titleSpan = document.createElement('span');
titleSpan.className = 'a11y-accordion-title';
titleSpan.textContent = item.title;
button.appendChild(titleSpan);
button.appendChild(icon);
// Region
const region = document.createElement('div');
region.className = 'a11y-accordion-region';
region.id = regionId;
region.setAttribute('role', 'region');
region.setAttribute('aria-labelledby', buttonId);
region.hidden = true;
if (typeof item.content === 'string') {
region.innerHTML = item.content;
}
else {
region.appendChild(item.content);
}
// Click handler
button.addEventListener('click', () => this.toggle(index));
// Keyboard handler
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.toggle(index);
}
});
this.items.push({ button, region, isOpen: false });
container.appendChild(button);
container.appendChild(region);
});
}
mount() {
// Nothing extra needed for accordion — click and keyboard are handled in constructor
}
toggle(index) {
const { button, region, isOpen } = this.items[index];
const newState = !isOpen;
this.items[index].isOpen = newState;
(0, helpers_1.setAria)(button, 'aria-expanded', newState);
if (newState) {
this.openRegion(region);
}
else {
this.closeRegion(region);
}
}
openRegion(region) {
region.hidden = false;
if ((0, helpers_1.prefersReducedMotion)()) {
region.style.height = 'auto';
return;
}
// Animate height
region.style.height = '0';
region.style.overflow = 'hidden';
region.style.transition = `height ${constants_1.ANIMATION.durationSlow} ${constants_1.ANIMATION.easing}`;
// Force reflow
region.offsetHeight;
const targetHeight = region.scrollHeight;
region.style.height = `${targetHeight}px`;
// After animation, set to auto
region.addEventListener('transitionend', () => {
region.style.height = 'auto';
region.style.overflow = '';
region.style.transition = '';
}, { once: true });
// Fallback
setTimeout(() => {
region.style.height = 'auto';
region.style.overflow = '';
region.style.transition = '';
}, 350);
}
closeRegion(region) {
if ((0, helpers_1.prefersReducedMotion)()) {
region.hidden = true;
return;
}
// Animate height to 0
region.style.height = `${region.scrollHeight}px`;
region.style.overflow = 'hidden';
region.style.transition = `height ${constants_1.ANIMATION.durationSlow} ${constants_1.ANIMATION.easingIn}`;
// Force reflow
region.offsetHeight;
region.style.height = '0';
// After animation, hide
region.addEventListener('transitionend', () => {
region.hidden = true;
region.style.height = '';
region.style.overflow = '';
region.style.transition = '';
}, { once: true });
// Fallback
setTimeout(() => {
region.hidden = true;
region.style.height = '';
region.style.overflow = '';
region.style.transition = '';
}, 350);
}
}
exports.Accordion = Accordion;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"d5fea801-c8b9-4d9e-a270-3a1bc7bc6ea2","tokensIn":2556500,"tokensOut":40412,"tokensTotal":2596912,"cost":0,"turns":54,"toolCalls":53,"failedToolCalls":0,"timestamp":"2026-08-01T16:11:03.778Z"} -->