Accessible Components
ling 3.0 Flash · 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 60,599 bytes · SHA-256 13816f7a741f
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Component Showcase</title>
<style>
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
DESIGN SYSTEM CONSTANTS
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
:root {
--color-bg: #f8f7f5;
--color-surface: #ffffff;
--color-border: #d4d0c8;
--color-text: #1a1a1a;
--color-text-secondary: #5a5650;
--color-text-muted: #8a8480;
--color-accent: #2b6cb0;
--color-accent-hover: #1e4f8a;
--color-accent-light: #e8f0fa;
--color-focus: #2b6cb0;
--color-focus-ring: rgba(43,108,176,0.45);
--color-danger: #c0392b;
--color-success: #278040;
--radius: 8px;
--radius-sm: 4px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
--font-mono: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", Menlo, Courier, monospace;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 24px; --space-6: 32px; --space-8: 48px;
--anim-fast: 150ms; --anim-normal: 250ms; --anim-slow: 350ms;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
RESET & BASE
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; scroll-behavior: smooth; }
body {
font-family: var(--font-sans);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
min-height: 100vh;
display: flex;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
SKIP LINK
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.skip-link {
position: absolute;
top: -100px;
left: var(--space-4);
background: var(--color-accent);
color: #fff;
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
z-index: 1000;
font-weight: 600;
text-decoration: none;
transition: top var(--anim-fast);
}
.skip-link:focus {
top: var(--space-4);
outline: 3px solid var(--color-focus);
outline-offset: 2px;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
LAYOUT
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.sidebar {
position: fixed;
top: 0; left: 0;
width: 220px;
height: 100vh;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
padding: var(--space-5) var(--space-4);
overflow-y: auto;
flex-shrink: 0;
}
.sidebar h1 {
font-size: 1.1rem;
margin-bottom: var(--space-4);
color: var(--color-accent);
letter-spacing: -0.01em;
}
.sidebar nav a {
display: block;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.9rem;
margin-bottom: 2px;
transition: background var(--anim-fast), color var(--anim-fast);
}
.sidebar nav a:hover,
.sidebar nav a:focus {
background: var(--color-accent-light);
color: var(--color-accent);
outline: none;
}
.sidebar nav a:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.main {
margin-left: 220px;
padding: var(--space-8) var(--space-8) var(--space-8) calc(220px + var(--space-8));
max-width: 900px;
flex: 1;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
FOCUS INDICATORS (global)
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
a:focus-visible,
[tabindex]:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
COMPONENT SECTIONS
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.component-section {
margin-bottom: var(--space-8);
padding: var(--space-6);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.component-section h2 {
font-size: 1.35rem;
margin-bottom: var(--space-2);
color: var(--color-text);
}
.component-section .caption {
color: var(--color-text-secondary);
font-size: 0.85rem;
margin-bottom: var(--space-5);
line-height: 1.5;
}
.component-section .caption kbd {
background: #eee;
border: 1px solid #ccc;
border-radius: 3px;
padding: 1px 5px;
font-family: var(--font-mono);
font-size: 0.8rem;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
MODAL
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 900;
opacity: 0;
visibility: hidden;
transition: opacity var(--anim-normal), visibility var(--anim-normal);
}
.modal-overlay.open {
opacity: 1;
visibility: visible;
}
.modal-dialog {
background: var(--color-surface);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: var(--space-6);
max-width: 480px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
transform: translateY(16px);
transition: transform var(--anim-normal);
}
.modal-overlay.open .modal-dialog {
transform: translateY(0);
}
.modal-dialog h3 {
margin-bottom: var(--space-4);
font-size: 1.15rem;
}
.modal-dialog p {
margin-bottom: var(--space-4);
color: var(--color-text-secondary);
}
.modal-dialog .btn-row {
display: flex;
gap: var(--space-3);
justify-content: flex-end;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
BUTTONS
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text);
font-size: 0.9rem;
cursor: pointer;
transition: background var(--anim-fast), border-color var(--anim-fast);
font-family: var(--font-sans);
}
.btn:hover {
background: #f0f0f0;
}
.btn-primary {
background: var(--color-accent);
color: #fff;
border-color: var(--color-accent);
}
.btn-primary:hover {
background: var(--color-accent-hover);
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
DROPDOWN MENU
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.dropdown-wrapper {
position: relative;
display: inline-block;
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 180px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: var(--space-2) 0;
z-index: 100;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: opacity var(--anim-fast), transform var(--anim-fast), visibility var(--anim-fast);
}
.dropdown-menu.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-menu [role="menuitem"] {
display: block;
width: 100%;
text-align: left;
padding: var(--space-2) var(--space-4);
background: none;
border: none;
cursor: pointer;
font-size: 0.9rem;
color: var(--color-text);
transition: background var(--anim-fast);
}
.dropdown-menu [role="menuitem"]:hover,
.dropdown-menu [role="menuitem"]:focus {
background: var(--color-accent-light);
}
.dropdown-menu [role="menuitem"].highlighted {
background: var(--color-accent);
color: #fff;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
TABS
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.tabs-nav {
display: flex;
gap: 2px;
border-bottom: 2px solid var(--color-border);
margin-bottom: var(--space-4);
}
.tabs-nav [role="tab"] {
padding: var(--space-2) var(--space-4);
border: none;
background: none;
cursor: pointer;
font-size: 0.9rem;
color: var(--color-text-secondary);
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color var(--anim-fast), border-color var(--anim-fast);
font-family: var(--font-sans);
}
.tabs-nav [role="tab"]:hover {
color: var(--color-text);
}
.tabs-nav [role="tab"][aria-selected="true"] {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
font-weight: 600;
}
.tabs-panel {
display: none;
}
.tabs-panel.visible {
display: block;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
COMBOBOX
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.combobox-wrapper {
position: relative;
width: 300px;
}
.combobox-wrapper input {
width: 100%;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
font-size: 0.95rem;
font-family: var(--font-sans);
}
.combobox-listbox {
position: absolute;
top: calc(100% + 2px);
left: 0;
right: 0;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
max-height: 200px;
overflow-y: auto;
z-index: 100;
opacity: 0;
visibility: hidden;
transform: translateY(-4px);
transition: opacity var(--anim-fast), transform var(--anim-fast), visibility var(--anim-fast);
}
.combobox-listbox.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.combobox-listbox [role="option"] {
padding: var(--space-2) var(--space-3);
cursor: pointer;
font-size: 0.9rem;
transition: background var(--anim-fast);
}
.combobox-listbox [role="option"]:hover,
.combobox-listbox [role="option"].highlighted {
background: var(--color-accent-light);
}
.combobox-listbox [role="option"].highlighted {
background: var(--color-accent);
color: #fff;
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
ACCORDION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
.accordion-item {
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
margin-bottom: var(--space-2);
overflow: hidden;
}
.accordion-header {
width: 100%;
text-align: left;
padding: var(--space-3) var(--space-4);
background: var(--color-surface);
border: none;
cursor: pointer;
font-size: 0.95rem;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-sans);
}
.accordion-header:hover {
background: #f5f5f5;
}
.accordion-header .arrow {
transition: transform var(--anim-normal);
font-size: 0.8rem;
}
.accordion-header[aria-expanded="true"] .arrow {
transform: rotate(90deg);
}
.accordion-panel {
max-height: 0;
overflow: hidden;
transition: max-height var(--anim-slow);
}
.accordion-panel[hidden] {
display: none;
}
.accordion-panel .inner {
padding: 0 var(--space-4) var(--space-4);
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
REDUCED MOTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
@media (max-width: 700px) {
.sidebar {
display: none;
}
.main {
margin-left: 0;
padding: var(--space-4);
}
}
</style>
</head>
<body>
<!-- SKIP LINK -->
<a class="skip-link" href="#main-content">Skip to main content</a>
<main id="main-content" class="main">
<!-- ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
MODAL SECTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -->
<section class="component-section" id="sec-modal" aria-labelledby="heading-modal">
<h2 id="heading-modal">Modal Dialog</h2>
<p class="caption">Opens a focused dialog; <kbd>Tab</kbd>/<kbd>Shift+Tab</kbd> cycle inside · <kbd>Esc</kbd> or click backdrop to close · focus returns to trigger</p>
<button class="btn btn-primary" id="modal-trigger">Open Modal</button>
</section>
<!-- Modal overlay -->
<div class="modal-overlay" id="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog" tabindex="-1">
<h3 id="modal-title">Modal Dialog</h3>
<p>This is a modal dialog. Focus is trapped inside while it is open. Press <kbd>Esc</kbd>, click the backdrop, or use the close button to dismiss it.</p>
<div class="btn-row">
<button class="btn" id="modal-close">Close</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
DROPDOWN MENU SECTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -->
<section class="component-section" id="sec-dropdown" aria-labelledby="heading-dropdown">
<h2 id="heading-dropdown">Dropdown Menu</h2>
<p class="caption"><kbd>Click</kbd> or <kbd>Enter</kbd>/<kbd>Space</kbd> to open · <kbd>↓</kbd> focuses first item · <kbd>↑↓</kbd> navigate · <kbd>Esc</kbd> closes · <kbd>Tab</kbd> moves onward</p>
<div class="dropdown-wrapper" id="dropdown-wrapper">
<button class="btn btn-primary" id="dropdown-trigger" aria-haspopup="menu" aria-expanded="false" aria-controls="dropdown-menu">Menu</button>
<div class="dropdown-menu" id="dropdown-menu" role="menu" aria-labelledby="dropdown-trigger">
<button role="menuitem" data-action="copy">Copy</button>
<button role="menuitem" data-action="paste">Paste</button>
<button role="menuitem" data-action="cut">Cut</button>
<button role="menuitem" data-action="select-all">Select All</button>
<button role="menuitem" data-action="delete">Delete</button>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
TABS SECTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -->
<section class="component-section" id="sec-tabs" aria-labelledby="heading-tabs">
<h2 id="heading-tabs">Tabs</h2>
<p class="caption"><kbd>←→</kbd> switch tabs · <kbd>Home/End</kbd> first/last · only active tab in tab order</p>
<div role="tablist" aria-label="Sample tabs" class="tabs-nav" id="tabs-tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">Tab One</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">Tab Two</button>
<button role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3" tabindex="-1">Tab Three</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1" class="tabs-panel visible">Content for Tab One. This panel is visible and reachable by Tab key.</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" class="tabs-panel" hidden>Content for Tab Two. Use the arrow keys to switch between tabs.</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" class="tabs-panel" hidden>Content for Tab Three. Only the active tab is focusable at a time.</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
COMBOBOX SECTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -->
<section class="component-section" id="sec-combobox" aria-labelledby="heading-combobox">
<h2 id="heading-combobox">Combobox (Autocomplete)</h2>
<p class="caption"><kbd>Type</kbd> to filter · <kbd>↓↑</kbd> navigate · <kbd>Enter</kbd> select · <kbd>Esc</kbd> closes · <kbd>Tab</kbd> moves onward</p>
<div class="combobox-wrapper" id="combobox-wrapper">
<input type="text" id="combobox-input" role="combobox" aria-expanded="false" aria-controls="combobox-listbox" aria-activedescendant="" aria-autocomplete="list" placeholder="Search…">
<div class="combobox-listbox" id="combobox-listbox" role="listbox" aria-label="Search suggestions">
<div role="option" id="opt-0" data-value="Apple">Apple</div>
<div role="option" id="opt-1" data-value="Apricot">Apricot</div>
<div role="option" id="opt-2" data-value="Banana">Banana</div>
<div role="option" id="opt-3" data-value="Blackberry">Blackberry</div>
<div role="option" id="opt-4" data-value="Blueberry">Blueberry</div>
<div role="option" id="opt-5" data-value="Cherry">Cherry</div>
<div role="option" id="opt-6" data-value="Coconut">Coconut</div>
<div role="option" id="opt-7" data-value="Cranberry">Cranberry</div>
<div role="option" id="opt-8" data-value="Date">Date</div>
<div role="option" id="opt-9" data-value="Dragonfruit">Dragonfruit</div>
<div role="option" id="opt-10" data-value="Fig">Fig</div>
<div role="option" id="opt-11" data-value="Grape">Grape</div>
<div role="option" id="opt-12" data-value="Guava">Guava</div>
<div role="option" id="opt-13" data-value="Kiwi">Kiwi</div>
<div role="option" id="opt-14" data-value="Lemon">Lemon</div>
<div role="option" id="opt-15" data-value="Lime">Lime</div>
<div role="option" id="opt-16" data-value="Mango">Mango</div>
<div role="option" id="opt-17" data-value="Nectarine">Nectarine</div>
<div role="option" id="opt-18" data-value="Orange">Orange</div>
<div role="option" id="opt-19" data-value="Papaya">Papaya</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
ACCORDION SECTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ -->
<section class="component-section" id="sec-accordion" aria-labelledby="heading-accordion">
<h2 id="heading-accordion">Accordion / Disclosure Group</h2>
<p class="caption"><kbd>Click</kbd> or <kbd>Enter</kbd>/<kbd>Space</kbd> to expand/collapse · multiple may be open · animate then toggle <code>hidden</code> at end</p>
<div class="accordion-item">
<button class="accordion-header" aria-expanded="false" aria-controls="acc-panel-1">First Section</button>
<div class="accordion-panel" id="acc-panel-1" hidden><div class="inner"><p>Content for the first section. Collapses smoothly when hidden.</p></div></div>
</div>
<div class="accordion-item">
<button class="accordion-header" aria-expanded="false" aria-controls="acc-panel-2">Second Section</button>
<div class="accordion-panel" id="acc-panel-2" hidden><div class="inner"><p>Content for the second section. Also supports multiple open panels at once.</p></div></div>
</div>
<div class="accordion-item">
<button class="accordion-header" aria-expanded="false" aria-controls="acc-panel-3">Third Section</button>
<div class="accordion-panel" id="acc-panel-3" hidden><div class="inner"><p>Content for the third section. Uses height animation then toggles hidden attribute.</p></div></div>
</div>
</section>
</main>
<script>
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
UTILITIES
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
var $ = function(sel, ctx) { return (ctx || document).querySelector(sel); };
var $$ = function(sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); };
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
FOCUS TRAP (reusable)
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
function FocusTrap(container, onReturn) {
this.container = container;
this.onReturn = onReturn;
this._prevFocus = null;
this._activate = this.activate.bind(this);
this._deactivate = this.deactivate.bind(this);
this._keydown = this._keydownHandler.bind(this);
}
FocusTrap.prototype.activate = function() {
this._prevFocus = document.activeElement;
this._focusable = this._getFocusable(this.container);
this._first = this._focusable[0] || null;
this._last = this._focusable[this._focusable.length - 1] || null;
if (this._first && this._first.focus) this._first.focus();
document.addEventListener('keydown', this._keydown);
};
FocusTrap.prototype.deactivate = function() {
document.removeEventListener('keydown', this._keydown);
if (this._prevFocus && this._prevFocus.focus) this._prevFocus.focus();
};
FocusTrap.prototype._getFocusable = function(el) {
var nodes = $$(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
el
).filter(function(e) {
var node = e;
while (node && node !== el) {
if (node.hasAttribute && node.getAttribute('aria-hidden') === 'true') return false;
node = node.parentElement;
}
return true;
});
return nodes;
};
FocusTrap.prototype._keydownHandler = function(e) {
if (e.key !== 'Tab') return;
var focusable = this._getFocusable(this.container);
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first || !this.container.contains(document.activeElement)) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last || !this.container.contains(document.activeElement)) {
e.preventDefault();
first.focus();
}
}
};
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
MODAL
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
var modalOverlay = $('#modal-overlay');
var modalTrigger = $('#modal-trigger');
var modalClose = $('#modal-close');
var modalDialog = modalOverlay.querySelector('.modal-dialog');
var modalTrap = null;
function openModal() {
modalOverlay.classList.add('open');
modalOverlay.setAttribute('aria-hidden', 'false');
modalDialog.focus();
modalTrap = new FocusTrap(modalDialog, function() {
modalOverlay.classList.remove('open');
modalOverlay.setAttribute('aria-hidden', 'true');
modalTrigger.focus();
});
modalTrap.activate();
}
function closeModal() {
modalOverlay.classList.remove('open');
modalOverlay.setAttribute('aria-hidden', 'true');
if (modalTrap) modalTrap.deactivate();
modalTrigger.focus();
}
modalTrigger.addEventListener('click', openModal);
modalClose.addEventListener('click', closeModal);
modalOverlay.addEventListener('click', function(e) {
if (e.target === modalOverlay) closeModal();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modalOverlay.classList.contains('open')) {
closeModal();
}
});
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
DROPDOWN MENU
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
var dropdownTrigger = $('#dropdown-trigger');
var dropdownMenu = $('#dropdown-menu');
var dropdownOpen = false;
function openDropdown() {
dropdownOpen = true;
dropdownTrigger.setAttribute('aria-expanded', 'true');
dropdownMenu.classList.add('open');
var firstItem = dropdownMenu.querySelector('[role="menuitem"]');
if (firstItem) firstItem.focus();
}
function closeDropdown() {
dropdownOpen = false;
dropdownTrigger.setAttribute('aria-expanded', 'false');
dropdownMenu.classList.remove('open');
dropdownTrigger.focus();
}
dropdownTrigger.addEventListener('click', function() {
if (dropdownOpen) closeDropdown(); else openDropdown();
});
dropdownTrigger.addEventListener('keydown', function(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
if (dropdownOpen) closeDropdown(); else openDropdown();
}
});
var menuItems = dropdownMenu.querySelectorAll('[role="menuitem"]');
var highlightedIdx = -1;
dropdownMenu.addEventListener('keydown', function(e) {
if (!dropdownOpen) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
highlightedIdx = (highlightedIdx + 1) % menuItems.length;
updateDropdownHighlight();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
highlightedIdx = (highlightedIdx - 1 + menuItems.length) % menuItems.length;
updateDropdownHighlight();
} else if (e.key === 'Escape') {
e.preventDefault();
closeDropdown();
} else if (e.key === 'Tab') {
closeDropdown();
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (highlightedIdx >= 0 && menuItems[highlightedIdx]) {
var action = menuItems[highlightedIdx].dataset.action;
if (action) {
alert('Action: ' + menuItems[highlightedIdx].textContent.trim());
}
closeDropdown();
}
}
});
function updateDropdownHighlight() {
for (var i = 0; i < menuItems.length; i++) {
if (i === highlightedIdx) {
menuItems[i].classList.add('highlighted');
menuItems[i].focus();
} else {
menuItems[i].classList.remove('highlighted');
}
}
}
dropdownMenu.addEventListener('click', function(e) {
var item = e.target.closest('[role="menuitem"]');
if (!item) return;
var action = item.dataset.action;
if (action) alert('Action: ' + item.textContent.trim());
closeDropdown();
});
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
TABS
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
var tablist = $('#tabs-tablist');
var tabs = tablist.querySelectorAll('[role="tab"]');
var panels = $$('[role="tabpanel"]');
function activateTab(tab) {
for (var i = 0; i < tabs.length; i++) {
var isActive = tabs[i] === tab;
tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
tabs[i].setAttribute('tabindex', isActive ? '0' : '-1');
}
for (var j = 0; j < panels.length; j++) {
panels[j].classList.remove('visible');
panels[j].hidden = true;
}
var panelId = tab.getAttribute('aria-controls');
var panel = $('#' + panelId);
if (panel) {
panel.classList.add('visible');
panel.hidden = false;
}
}
tablist.addEventListener('keydown', function(e) {
var currentTab = document.activeElement;
if (!currentTab || currentTab.getAttribute('role') !== 'tab') return;
var tabArr = Array.prototype.slice.call(tabs);
var idx = tabArr.indexOf(currentTab);
if (idx === -1) return;
if (e.key === 'ArrowRight') {
e.preventDefault();
var next = tabArr[(idx + 1) % tabArr.length];
activateTab(next);
next.focus();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
var prev = tabArr[(idx - 1 + tabArr.length) % tabArr.length];
activateTab(prev);
prev.focus();
} else if (e.key === 'Home') {
e.preventDefault();
activateTab(tabs[0]);
tabs[0].focus();
} else if (e.key === 'End') {
e.preventDefault();
activateTab(tabs[tabs.length - 1]);
tabs[tabs.length - 1].focus();
}
});
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
COMBOBOX
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
var comboboxInput = $('#combobox-input');
var comboboxListbox = $('#combobox-listbox');
var allOptions = Array.prototype.slice.call(comboboxListbox.querySelectorAll('[role="option"]'));
var comboboxHighlighted = -1;
var comboboxOpen = false;
function updateCombobox() {
var val = comboboxInput.value.toLowerCase();
var filtered = allOptions.filter(function(opt) {
return opt.textContent.toLowerCase().indexOf(val) !== -1;
});
for (var i = 0; i < allOptions.length; i++) {
allOptions[i].style.display = 'none';
allOptions[i].classList.remove('highlighted');
}
if (filtered.length > 0) {
comboboxListbox.classList.add('open');
comboboxOpen = true;
} else {
comboboxListbox.classList.remove('open');
comboboxOpen = false;
}
comboboxInput.setAttribute('aria-expanded', comboboxOpen ? 'true' : 'false');
comboboxHighlighted = -1;
if (comboboxOpen && filtered.length > 0) {
for (var j = 0; j < filtered.length; j++) {
filtered[j].style.display = '';
}
}
}
function highlightOption(idx) {
var visible = Array.prototype.slice.call(comboboxListbox.querySelectorAll('[role="option"]')).filter(function(o) {
return o.style.display !== 'none';
});
if (!visible.length) return;
comboboxHighlighted = Math.max(0, Math.min(idx, visible.length - 1));
for (var i = 0; i < visible.length; i++) {
if (i === comboboxHighlighted) {
visible[i].classList.add('highlighted');
visible[i].setAttribute('aria-selected', 'true');
visible[i].scrollIntoView({ block: 'nearest' });
} else {
visible[i].classList.remove('highlighted');
visible[i].setAttribute('aria-selected', 'false');
}
}
var activeId = comboboxHighlighted >= 0 ? visible[comboboxHighlighted].id : '';
comboboxInput.setAttribute('aria-activedescendant', activeId);
}
comboboxInput.addEventListener('input', updateCombobox);
comboboxInput.addEventListener('focus', function() {
var visible = Array.prototype.slice.call(allOptions).filter(function(o) {
return o.style.display !== 'none';
});
if (visible.length > 0) {
comboboxListbox.classList.add('open');
comboboxOpen = true;
comboboxInput.setAttribute('aria-expanded', 'true');
highlightOption(0);
}
});
comboboxInput.addEventListener('keydown', function(e) {
if (!comboboxOpen) return;
var visible = Array.prototype.slice.call(comboboxListbox.querySelectorAll('[role="option"]')).filter(function(o) {
return o.style.display !== 'none';
});
if (!visible.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
highlightOption(comboboxHighlighted + 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
highlightOption(comboboxHighlighted - 1);
} else if (e.key === 'Enter') {
e.preventDefault();
if (comboboxHighlighted >= 0) {
var opt = visible[comboboxHighlighted];
comboboxInput.value = opt.textContent;
comboboxInput.setAttribute('aria-activedescendant', opt.id);
}
comboboxInput.setAttribute('aria-expanded', 'false');
comboboxListbox.classList.remove('open');
comboboxOpen = false;
} else if (e.key === 'Escape') {
e.preventDefault();
comboboxInput.setAttribute('aria-expanded', 'false');
comboboxListbox.classList.remove('open');
comboboxOpen = false;
} else if (e.key === 'Tab') {
comboboxInput.setAttribute('aria-expanded', 'false');
comboboxListbox.classList.remove('open');
comboboxOpen = false;
}
});
document.addEventListener('click', function(e) {
if (comboboxOpen && !comboboxInput.contains(e.target) && !comboboxListbox.contains(e.target)) {
comboboxInput.setAttribute('aria-expanded', 'false');
comboboxListbox.classList.remove('open');
comboboxOpen = false;
}
});
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
ACCORDION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
var accordionHeaders = $$('.accordion-header');
for (var i = 0; i < accordionHeaders.length; i++) {
(function(header) {
header.addEventListener('click', function() {
toggleAccordion(header);
});
header.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleAccordion(header);
}
});
})(accordionHeaders[i]);
}
function toggleAccordion(header) {
var panelId = header.getAttribute('aria-controls');
var panel = document.getElementById(panelId);
if (!panel) return;
var isExpanded = header.getAttribute('aria-expanded') === 'true';
header.setAttribute('aria-expanded', isExpanded ? 'false' : 'true');
if (isExpanded) {
panel.style.maxHeight = panel.scrollHeight + 'px';
panel.offsetHeight;
panel.style.maxHeight = '0px';
var onEnd = function() {
panel.hidden = true;
panel.style.maxHeight = '';
panel.removeEventListener('transitionend', onEnd);
};
panel.addEventListener('transitionend', onEnd);
setTimeout(function() {
if (!panel.hidden) {
panel.hidden = true;
panel.style.maxHeight = '';
}
}, 400);
} else {
panel.hidden = false;
panel.style.maxHeight = '0px';
panel.offsetHeight;
panel.style.maxHeight = panel.scrollHeight + 'px';
var onEnd2 = function() {
panel.style.maxHeight = '';
panel.removeEventListener('transitionend', onEnd2);
};
panel.addEventListener('transitionend', onEnd2);
}
}
/* ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
REDUCED MOTION
═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ */
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
var accordionPanels = $$('.accordion-panel');
for (var i = 0; i < accordionPanels.length; i++) {
accordionPanels[i].style.transition = 'none';
}
}
</script>
</body>
</html>
<!-- agent-meta {"model":"inclusionai/ling-3.0-flash","provider":"openrouter","persona":"full","sessionId":"d040ec5b-27e9-4876-8fd4-6ce0fe127455","tokensIn":420664,"tokensOut":37207,"tokensTotal":457871,"cost":0.014367984,"turns":13,"toolCalls":12,"failedToolCalls":0,"timestamp":"2026-08-24T23:13:07.400Z"} -->