My agentic slop goes here. Not intended for anyone else!

web

+186
arod-web/README.md
···
+
# Arod Web Components Library
+
+
A collection of modern, accessible web components for building content-rich websites with semantic HTML.
+
+
## Components
+
+
### ๐Ÿ—‚๏ธ Navigation Menu (`arod-menu`)
+
A responsive navigation menu with mobile support and customizable branding.
+
+
**Features:**
+
- Responsive hamburger menu on mobile
+
- Active item highlighting
+
- Customizable logo/brand
+
- Action slots for search and theme toggle
+
+
### ๐Ÿ” Search Modal (`arod-search`)
+
An expandable search interface with keyboard shortcuts and real-time results.
+
+
**Features:**
+
- Keyboard shortcut (โŒ˜K / Ctrl+K) to open
+
- Real-time search with debouncing
+
- Keyboard navigation (arrow keys + enter)
+
- Customizable result display
+
- Event-based integration with any search backend
+
+
### ๐ŸŽจ Theme Toggle (`arod-theme`)
+
A theme switcher supporting light, dark, and system preferences.
+
+
**Features:**
+
- Light/Dark/System modes
+
- Persists preference in localStorage
+
- Respects system preferences
+
- Updates all components automatically
+
- Smooth transitions
+
+
### ๐Ÿ“ Sidenotes (`article-sidenotes`)
+
A sophisticated sidenote system for rich annotations and margin notes.
+
+
**Features:**
+
- Automatic numbering
+
- Responsive: margin notes on desktop, inline on mobile
+
- Bidirectional hover highlighting
+
- Support for rich content (images, code blocks)
+
- Semantic HTML structure
+
+
## Installation
+
+
1. Copy the `arod/` directory to your project
+
2. Include the component scripts you need:
+
+
```html
+
<script src="arod/arod-menu.js"></script>
+
<script src="arod/arod-search.js"></script>
+
<script src="arod/arod-theme.js"></script>
+
<script src="arod/arod-sidenotes.js"></script>
+
```
+
+
## Usage Examples
+
+
### Basic Navigation Setup
+
+
```html
+
<arod-menu>
+
<menu-item href="/home">Home</menu-item>
+
<menu-item href="/docs">Documentation</menu-item>
+
<menu-item href="/about">About</menu-item>
+
+
<arod-search slot="actions"></arod-search>
+
<arod-theme slot="actions"></arod-theme>
+
</arod-menu>
+
```
+
+
### Sidenotes in Articles
+
+
```html
+
<article-sidenotes>
+
<p>
+
Your article text with a reference<sidenote-ref for="note1"></sidenote-ref>
+
to additional information.
+
</p>
+
+
<side-note id="note1">
+
This appears in the margin on desktop and inline on mobile.
+
</side-note>
+
</article-sidenotes>
+
```
+
+
### Search Integration
+
+
```javascript
+
const search = document.querySelector('arod-search');
+
+
search.addEventListener('search', async (e) => {
+
const query = e.detail.query;
+
+
// Fetch results from your search backend
+
const results = await fetchSearchResults(query);
+
+
// Display results in the search modal
+
search.displayResults(results);
+
});
+
```
+
+
### Theme Integration
+
+
```javascript
+
// Listen for theme changes
+
document.querySelector('arod-theme').addEventListener('theme-change', (e) => {
+
console.log('Theme changed to:', e.detail.theme);
+
// Update any non-Arod components if needed
+
});
+
```
+
+
## Styling
+
+
Components use CSS custom properties for theming:
+
+
```css
+
:root {
+
--accent: #0066cc;
+
--bg: #ffffff;
+
--text: #222;
+
--text-muted: #666;
+
--border: #e0e0e0;
+
}
+
+
[data-theme="dark"] {
+
--bg: #1a1a1a;
+
--text: #e0e0e0;
+
--text-muted: #999;
+
--accent: #4db8ff;
+
--border: #444;
+
}
+
```
+
+
## Browser Support
+
+
- Chrome/Edge 90+
+
- Firefox 88+
+
- Safari 14+
+
+
Web Components APIs are required. Consider polyfills for older browsers.
+
+
## Component Details
+
+
### Arod Menu Events
+
- None (navigation handled via standard links)
+
+
### Arod Search Events
+
- `search`: Fired when user types (debounced)
+
- `clear`: Fired when search is cleared
+
+
### Arod Theme Events
+
- `theme-change`: Fired when theme changes
+
+
### Article Sidenotes Events
+
- `sidenote-hover`: Internal event for highlighting
+
- `ref-hover`: Internal event for highlighting
+
+
## Architecture
+
+
All components:
+
- Use Shadow DOM for style encapsulation
+
- Follow semantic HTML principles
+
- Support keyboard navigation
+
- Are ARIA compliant
+
- Work independently or together
+
- Use custom elements v1 specification
+
+
## Examples
+
+
See `example.html` for a complete working demo of all components.
+
+
## License
+
+
MIT
+
+
## Contributing
+
+
Contributions welcome! Please ensure new components:
+
- Follow the `arod-[name]` naming convention
+
- Use Shadow DOM for encapsulation
+
- Emit appropriate custom events
+
- Include keyboard support
+
- Are accessible (ARIA labels, roles, etc.)
+
- Work independently of other components
+261
arod-web/arod-menu.js
···
+
/**
+
* Arod Menu Bar Component
+
* A responsive navigation menu with search and theme toggle
+
*
+
* Usage:
+
* <arod-menu>
+
* <menu-item href="/papers">Papers</menu-item>
+
* <menu-item href="/notes">Notes</menu-item>
+
* <menu-item href="/about">About</menu-item>
+
* </arod-menu>
+
*/
+
+
// ============================================
+
// AROD-MENU - Main Menu Bar Component
+
// ============================================
+
class ArodMenu extends HTMLElement {
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
}
+
+
connectedCallback() {
+
this.render();
+
this.setupMobileToggle();
+
}
+
+
render() {
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
--menu-height: 3.5rem;
+
--menu-bg: #ffffff;
+
--menu-border: #e0e0e0;
+
--menu-text: #222;
+
--menu-hover: #0066cc;
+
--menu-mobile-breakpoint: 768px;
+
+
display: block;
+
position: sticky;
+
top: 0;
+
z-index: 1000;
+
background: var(--menu-bg);
+
border-bottom: 1px solid var(--menu-border);
+
height: var(--menu-height);
+
}
+
+
@media (prefers-color-scheme: dark) {
+
:host {
+
--menu-bg: #1a1a1a;
+
--menu-border: #444;
+
--menu-text: #e0e0e0;
+
--menu-hover: #4db8ff;
+
}
+
}
+
+
:host([theme="light"]) {
+
--menu-bg: #ffffff;
+
--menu-border: #e0e0e0;
+
--menu-text: #222;
+
--menu-hover: #0066cc;
+
}
+
+
:host([theme="dark"]) {
+
--menu-bg: #1a1a1a;
+
--menu-border: #444;
+
--menu-text: #e0e0e0;
+
--menu-hover: #4db8ff;
+
}
+
+
.menu-container {
+
display: flex;
+
align-items: center;
+
justify-content: space-between;
+
height: 100%;
+
max-width: 1200px;
+
margin: 0 auto;
+
padding: 0 1rem;
+
}
+
+
.menu-left {
+
display: flex;
+
align-items: center;
+
gap: 2rem;
+
}
+
+
.menu-logo {
+
font-size: 1.25rem;
+
font-weight: 600;
+
color: var(--menu-text);
+
text-decoration: none;
+
}
+
+
.menu-items {
+
display: flex;
+
align-items: center;
+
gap: 1.5rem;
+
}
+
+
.menu-right {
+
display: flex;
+
align-items: center;
+
gap: 1rem;
+
}
+
+
.mobile-toggle {
+
display: none;
+
background: none;
+
border: none;
+
color: var(--menu-text);
+
cursor: pointer;
+
padding: 0.5rem;
+
}
+
+
.mobile-toggle svg {
+
width: 24px;
+
height: 24px;
+
}
+
+
@media (max-width: 768px) {
+
.menu-items {
+
display: none;
+
position: absolute;
+
top: var(--menu-height);
+
left: 0;
+
right: 0;
+
background: var(--menu-bg);
+
border-bottom: 1px solid var(--menu-border);
+
flex-direction: column;
+
padding: 1rem;
+
gap: 0.5rem;
+
align-items: stretch;
+
}
+
+
.menu-items.mobile-open {
+
display: flex;
+
}
+
+
.mobile-toggle {
+
display: block;
+
}
+
+
.menu-right {
+
margin-left: auto;
+
}
+
}
+
+
::slotted(menu-item) {
+
color: var(--menu-text);
+
text-decoration: none;
+
padding: 0.5rem 0.75rem;
+
border-radius: 4px;
+
transition: all 0.2s ease;
+
cursor: pointer;
+
}
+
+
::slotted(menu-item:hover) {
+
color: var(--menu-hover);
+
background: rgba(0, 102, 204, 0.1);
+
}
+
+
::slotted(menu-item[active]) {
+
color: var(--menu-hover);
+
font-weight: 500;
+
}
+
</style>
+
+
<nav class="menu-container">
+
<div class="menu-left">
+
<a href="/" class="menu-logo">AROD</a>
+
<button class="mobile-toggle" aria-label="Menu">
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
+
</svg>
+
</button>
+
<div class="menu-items">
+
<slot></slot>
+
</div>
+
</div>
+
<div class="menu-right">
+
<slot name="actions"></slot>
+
</div>
+
</nav>
+
`;
+
}
+
+
setupMobileToggle() {
+
const toggle = this.shadowRoot.querySelector('.mobile-toggle');
+
const items = this.shadowRoot.querySelector('.menu-items');
+
+
toggle?.addEventListener('click', () => {
+
items?.classList.toggle('mobile-open');
+
});
+
+
// Close menu when clicking outside
+
document.addEventListener('click', (e) => {
+
if (!this.contains(e.target)) {
+
items?.classList.remove('mobile-open');
+
}
+
});
+
}
+
}
+
+
// ============================================
+
// MENU-ITEM - Individual Menu Item Component
+
// ============================================
+
class MenuItem extends HTMLElement {
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
}
+
+
connectedCallback() {
+
this.render();
+
this.setupNavigation();
+
}
+
+
render() {
+
const href = this.getAttribute('href') || '#';
+
const label = this.textContent;
+
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
display: inline-block;
+
}
+
+
a {
+
color: inherit;
+
text-decoration: none;
+
padding: 0.5rem 0.75rem;
+
border-radius: 4px;
+
transition: all 0.2s ease;
+
display: block;
+
}
+
+
:host(:hover) a {
+
color: var(--menu-hover, #0066cc);
+
background: rgba(0, 102, 204, 0.1);
+
}
+
+
:host([active]) a {
+
color: var(--menu-hover, #0066cc);
+
font-weight: 500;
+
}
+
</style>
+
<a href="${href}">${label}</a>
+
`;
+
}
+
+
setupNavigation() {
+
// Mark active based on current path
+
const href = this.getAttribute('href');
+
if (href && window.location.pathname === href) {
+
this.setAttribute('active', '');
+
}
+
}
+
}
+
+
// Register components
+
customElements.define('arod-menu', ArodMenu);
+
customElements.define('menu-item', MenuItem);
+429
arod-web/arod-search.js
···
+
/**
+
* Arod Search Modal Component
+
* An expandable search interface with modal overlay
+
*
+
* Usage:
+
* <arod-search placeholder="Search docs..."></arod-search>
+
*
+
* Events:
+
* - search: Fired when user performs a search
+
* - clear: Fired when search is cleared
+
*/
+
+
// ============================================
+
// AROD-SEARCH - Search Modal Component
+
// ============================================
+
class ArodSearch extends HTMLElement {
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
this.isOpen = false;
+
this.searchDebounce = null;
+
}
+
+
connectedCallback() {
+
this.render();
+
this.setupEventListeners();
+
this.setupKeyboardShortcuts();
+
}
+
+
disconnectedCallback() {
+
this.removeKeyboardShortcuts();
+
}
+
+
render() {
+
const placeholder = this.getAttribute('placeholder') || 'Search...';
+
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
--search-bg: #ffffff;
+
--search-border: #e0e0e0;
+
--search-text: #222;
+
--search-muted: #666;
+
--search-accent: #0066cc;
+
--search-overlay: rgba(0, 0, 0, 0.5);
+
}
+
+
@media (prefers-color-scheme: dark) {
+
:host {
+
--search-bg: #1a1a1a;
+
--search-border: #444;
+
--search-text: #e0e0e0;
+
--search-muted: #999;
+
--search-accent: #4db8ff;
+
--search-overlay: rgba(0, 0, 0, 0.8);
+
}
+
}
+
+
.search-trigger {
+
display: flex;
+
align-items: center;
+
gap: 0.5rem;
+
padding: 0.5rem 0.75rem;
+
background: transparent;
+
border: 1px solid var(--search-border);
+
border-radius: 4px;
+
color: var(--search-text);
+
cursor: pointer;
+
transition: all 0.2s ease;
+
}
+
+
.search-trigger:hover {
+
border-color: var(--search-accent);
+
background: rgba(0, 102, 204, 0.05);
+
}
+
+
.search-icon {
+
width: 18px;
+
height: 18px;
+
}
+
+
.search-shortcut {
+
font-size: 0.75rem;
+
padding: 0.125rem 0.375rem;
+
background: var(--search-border);
+
border-radius: 3px;
+
color: var(--search-muted);
+
font-family: monospace;
+
}
+
+
.modal-overlay {
+
display: none;
+
position: fixed;
+
top: 0;
+
left: 0;
+
right: 0;
+
bottom: 0;
+
background: var(--search-overlay);
+
z-index: 9999;
+
animation: fadeIn 0.2s ease;
+
}
+
+
.modal-overlay.open {
+
display: block;
+
}
+
+
.modal-container {
+
position: fixed;
+
top: 10vh;
+
left: 50%;
+
transform: translateX(-50%);
+
width: 90%;
+
max-width: 600px;
+
background: var(--search-bg);
+
border-radius: 8px;
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+
animation: slideDown 0.3s ease;
+
}
+
+
.search-header {
+
position: relative;
+
padding: 1rem;
+
border-bottom: 1px solid var(--search-border);
+
}
+
+
.search-input-wrapper {
+
display: flex;
+
align-items: center;
+
gap: 0.75rem;
+
}
+
+
.search-input {
+
flex: 1;
+
padding: 0.75rem;
+
font-size: 1.125rem;
+
background: transparent;
+
border: none;
+
color: var(--search-text);
+
outline: none;
+
}
+
+
.search-input::placeholder {
+
color: var(--search-muted);
+
}
+
+
.search-close {
+
padding: 0.5rem;
+
background: transparent;
+
border: none;
+
color: var(--search-muted);
+
cursor: pointer;
+
border-radius: 4px;
+
transition: all 0.2s ease;
+
}
+
+
.search-close:hover {
+
background: var(--search-border);
+
}
+
+
.search-results {
+
max-height: 60vh;
+
overflow-y: auto;
+
padding: 0.5rem;
+
}
+
+
.search-results:empty::after {
+
content: "Type to search...";
+
display: block;
+
padding: 2rem;
+
text-align: center;
+
color: var(--search-muted);
+
}
+
+
.search-result-item {
+
padding: 0.75rem 1rem;
+
border-radius: 4px;
+
cursor: pointer;
+
transition: all 0.2s ease;
+
}
+
+
.search-result-item:hover {
+
background: rgba(0, 102, 204, 0.1);
+
}
+
+
.result-title {
+
font-weight: 500;
+
color: var(--search-text);
+
margin-bottom: 0.25rem;
+
}
+
+
.result-snippet {
+
font-size: 0.875rem;
+
color: var(--search-muted);
+
line-height: 1.4;
+
}
+
+
.result-snippet mark {
+
background: rgba(255, 200, 0, 0.3);
+
color: inherit;
+
padding: 0.1em 0.2em;
+
border-radius: 2px;
+
}
+
+
.search-footer {
+
padding: 0.75rem 1rem;
+
border-top: 1px solid var(--search-border);
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
font-size: 0.875rem;
+
color: var(--search-muted);
+
}
+
+
.search-hints {
+
display: flex;
+
gap: 1rem;
+
}
+
+
.search-hint {
+
display: flex;
+
align-items: center;
+
gap: 0.25rem;
+
}
+
+
.key {
+
padding: 0.125rem 0.375rem;
+
background: var(--search-border);
+
border-radius: 3px;
+
font-family: monospace;
+
}
+
+
@keyframes fadeIn {
+
from { opacity: 0; }
+
to { opacity: 1; }
+
}
+
+
@keyframes slideDown {
+
from {
+
opacity: 0;
+
transform: translateX(-50%) translateY(-20px);
+
}
+
to {
+
opacity: 1;
+
transform: translateX(-50%) translateY(0);
+
}
+
}
+
</style>
+
+
<button class="search-trigger" aria-label="Search">
+
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
+
</svg>
+
<span class="search-shortcut">โŒ˜K</span>
+
</button>
+
+
<div class="modal-overlay">
+
<div class="modal-container">
+
<div class="search-header">
+
<div class="search-input-wrapper">
+
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
+
</svg>
+
<input type="text" class="search-input" placeholder="${placeholder}" autocomplete="off">
+
<button class="search-close" aria-label="Close">
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+
</svg>
+
</button>
+
</div>
+
</div>
+
+
<div class="search-results"></div>
+
+
<div class="search-footer">
+
<div class="search-hints">
+
<span class="search-hint">
+
<span class="key">โ†‘โ†“</span> navigate
+
</span>
+
<span class="search-hint">
+
<span class="key">โ†ต</span> select
+
</span>
+
<span class="search-hint">
+
<span class="key">esc</span> close
+
</span>
+
</div>
+
<div class="search-powered">
+
<slot name="powered-by"></slot>
+
</div>
+
</div>
+
</div>
+
</div>
+
`;
+
}
+
+
setupEventListeners() {
+
const trigger = this.shadowRoot.querySelector('.search-trigger');
+
const overlay = this.shadowRoot.querySelector('.modal-overlay');
+
const close = this.shadowRoot.querySelector('.search-close');
+
const input = this.shadowRoot.querySelector('.search-input');
+
+
trigger.addEventListener('click', () => this.open());
+
close.addEventListener('click', () => this.close());
+
+
overlay.addEventListener('click', (e) => {
+
if (e.target === overlay) this.close();
+
});
+
+
input.addEventListener('input', (e) => {
+
clearTimeout(this.searchDebounce);
+
this.searchDebounce = setTimeout(() => {
+
this.handleSearch(e.target.value);
+
}, 200);
+
});
+
+
input.addEventListener('keydown', (e) => {
+
if (e.key === 'Escape') this.close();
+
else if (e.key === 'ArrowDown') this.navigateResults(1);
+
else if (e.key === 'ArrowUp') this.navigateResults(-1);
+
else if (e.key === 'Enter') this.selectResult();
+
});
+
}
+
+
setupKeyboardShortcuts() {
+
this.keyHandler = (e) => {
+
// Cmd+K or Ctrl+K to open search
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+
e.preventDefault();
+
this.open();
+
}
+
};
+
document.addEventListener('keydown', this.keyHandler);
+
}
+
+
removeKeyboardShortcuts() {
+
if (this.keyHandler) {
+
document.removeEventListener('keydown', this.keyHandler);
+
}
+
}
+
+
open() {
+
this.isOpen = true;
+
const overlay = this.shadowRoot.querySelector('.modal-overlay');
+
const input = this.shadowRoot.querySelector('.search-input');
+
+
overlay.classList.add('open');
+
setTimeout(() => input.focus(), 100);
+
}
+
+
close() {
+
this.isOpen = false;
+
const overlay = this.shadowRoot.querySelector('.modal-overlay');
+
const input = this.shadowRoot.querySelector('.search-input');
+
const results = this.shadowRoot.querySelector('.search-results');
+
+
overlay.classList.remove('open');
+
input.value = '';
+
results.innerHTML = '';
+
}
+
+
handleSearch(query) {
+
if (!query) {
+
this.clearResults();
+
return;
+
}
+
+
// Dispatch search event for external handling
+
this.dispatchEvent(new CustomEvent('search', {
+
detail: { query },
+
bubbles: true
+
}));
+
}
+
+
// Public method to display search results
+
displayResults(results) {
+
const container = this.shadowRoot.querySelector('.search-results');
+
container.innerHTML = '';
+
+
if (!results || results.length === 0) {
+
container.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--search-muted);">No results found</div>';
+
return;
+
}
+
+
results.forEach((result, index) => {
+
const item = document.createElement('div');
+
item.className = 'search-result-item';
+
item.dataset.index = index;
+
item.innerHTML = `
+
<div class="result-title">${result.title}</div>
+
<div class="result-snippet">${result.snippet || ''}</div>
+
`;
+
item.addEventListener('click', () => {
+
if (result.url) window.location.href = result.url;
+
this.close();
+
});
+
container.appendChild(item);
+
});
+
}
+
+
clearResults() {
+
const container = this.shadowRoot.querySelector('.search-results');
+
container.innerHTML = '';
+
+
this.dispatchEvent(new CustomEvent('clear', { bubbles: true }));
+
}
+
+
navigateResults(direction) {
+
const results = this.shadowRoot.querySelectorAll('.search-result-item');
+
if (!results.length) return;
+
+
const current = this.shadowRoot.querySelector('.search-result-item.selected');
+
let index = current ? parseInt(current.dataset.index) : -1;
+
+
index += direction;
+
if (index < 0) index = results.length - 1;
+
if (index >= results.length) index = 0;
+
+
results.forEach(r => r.classList.remove('selected'));
+
results[index].classList.add('selected');
+
results[index].scrollIntoView({ block: 'nearest' });
+
}
+
+
selectResult() {
+
const selected = this.shadowRoot.querySelector('.search-result-item.selected');
+
if (selected) selected.click();
+
}
+
}
+
+
// Register component
+
customElements.define('arod-search', ArodSearch);
+531
arod-web/arod-sidenotes.js
···
+
/**
+
* Sidenotes Web Components
+
*
+
* Usage:
+
* <article-sidenotes>
+
* <p>Text with reference<sidenote-ref for="note1"></sidenote-ref></p>
+
* <side-note id="note1">Note content</side-note>
+
* </article-sidenotes>
+
*/
+
+
// ============================================
+
// ARTICLE-SIDENOTES - Main Container Component
+
// ============================================
+
class ArticleSidenotes extends HTMLElement {
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
this._noteCounter = 0;
+
this._noteMap = new Map();
+
this._resizeTimeout = null;
+
}
+
+
connectedCallback() {
+
this.render();
+
// Wait for DOM to be ready
+
setTimeout(() => {
+
this.processNotes();
+
this.setupPositioning();
+
this.setupHighlighting();
+
}, 0);
+
}
+
+
disconnectedCallback() {
+
if (this._resizeTimeout) {
+
clearTimeout(this._resizeTimeout);
+
}
+
}
+
+
render() {
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
--content-width: 38rem;
+
--sidenote-width: 18rem;
+
--gap: 2.5rem;
+
--bg: #ffffff;
+
--text: #222;
+
--text-muted: #666;
+
--accent: #0066cc;
+
--sidenote-bg: #fafafa;
+
--border: #e0e0e0;
+
+
display: block;
+
position: relative;
+
max-width: calc(var(--content-width) + var(--gap) + var(--sidenote-width));
+
margin: 0 auto;
+
padding: 0 1rem;
+
}
+
+
@media (prefers-color-scheme: dark) {
+
:host {
+
--bg: #1a1a1a;
+
--text: #e0e0e0;
+
--text-muted: #999;
+
--accent: #4db8ff;
+
--sidenote-bg: #252525;
+
--border: #444;
+
}
+
}
+
+
.content {
+
max-width: var(--content-width);
+
}
+
+
.sidenotes-container {
+
display: none;
+
}
+
+
/* Only show sidenotes in margin when there's enough space */
+
/* 38rem + 2.5rem + 18rem + 2rem padding = 60.5rem โ‰ˆ 968px */
+
@media (min-width: 980px) {
+
:host {
+
padding: 0 1rem 0 0.5rem;
+
}
+
+
.sidenotes-container {
+
display: block;
+
position: absolute;
+
left: calc(var(--content-width) + var(--gap));
+
top: 0;
+
width: var(--sidenote-width);
+
height: 100%;
+
}
+
+
.sidenote-wrapper {
+
position: absolute;
+
width: 100%;
+
font-size: 0.75rem;
+
line-height: 1.4;
+
color: var(--text-muted);
+
padding-left: 1rem;
+
border-left: 2px solid var(--border);
+
transition: all 0.2s ease;
+
}
+
+
.sidenote-wrapper:hover,
+
.sidenote-wrapper.highlighted {
+
border-left-color: var(--accent);
+
color: var(--text);
+
}
+
+
.sidenote-wrapper.highlighted {
+
background: var(--accent);
+
color: white;
+
padding: 0.5rem 0.5rem 0.5rem 1rem;
+
border-radius: 0 4px 4px 0;
+
margin-left: -2px;
+
}
+
+
.sidenote-number {
+
display: inline;
+
font-weight: bold;
+
color: var(--accent);
+
margin-right: 0.3em;
+
}
+
+
.sidenote-wrapper.highlighted .sidenote-number {
+
color: white;
+
}
+
+
.sidenote-content {
+
display: inline;
+
}
+
+
.sidenote-content img {
+
display: block;
+
width: 100%;
+
height: auto;
+
margin: 0.5rem 0;
+
border-radius: 4px;
+
}
+
+
.sidenote-content pre {
+
background: var(--sidenote-bg);
+
padding: 0.5rem;
+
border-radius: 4px;
+
overflow-x: auto;
+
}
+
+
.sidenote-wrapper.highlighted .sidenote-content pre {
+
background: rgba(255, 255, 255, 0.2);
+
}
+
}
+
+
::slotted(*) {
+
/* Ensure slotted content inherits styles */
+
}
+
</style>
+
+
<div class="content">
+
<slot></slot>
+
</div>
+
<div class="sidenotes-container" id="sidenotes-container"></div>
+
`;
+
}
+
+
processNotes() {
+
// Auto-number sidenotes and refs
+
let counter = 1;
+
+
// Find all sidenote elements
+
const sidenotes = this.querySelectorAll('side-note');
+
const refs = this.querySelectorAll('sidenote-ref');
+
+
// Create a map of note IDs to numbers
+
sidenotes.forEach(note => {
+
const id = note.getAttribute('id');
+
if (id) {
+
this._noteMap.set(id, counter);
+
note.setAttribute('data-number', counter);
+
counter++;
+
}
+
});
+
+
// Apply numbers to refs
+
refs.forEach(ref => {
+
const forId = ref.getAttribute('for');
+
if (forId && this._noteMap.has(forId)) {
+
ref.setAttribute('data-number', this._noteMap.get(forId));
+
}
+
});
+
}
+
+
setupPositioning() {
+
const container = this.shadowRoot.getElementById('sidenotes-container');
+
const sidenotes = this.querySelectorAll('side-note');
+
+
// Calculate minimum width needed for sidenotes in margin
+
// 38rem + 2.5rem + 18rem + 1.5rem padding = 60rem โ‰ˆ 980px
+
const minWidthForSidenotes = 980;
+
+
// Clear existing clones
+
container.innerHTML = '';
+
+
// Only clone if we have enough space
+
if (window.innerWidth >= minWidthForSidenotes) {
+
// Clone sidenotes to the container for desktop display
+
sidenotes.forEach(note => {
+
// Create a wrapper div to hold the sidenote
+
const wrapper = document.createElement('div');
+
wrapper.className = 'sidenote-wrapper';
+
wrapper.setAttribute('data-number', note.getAttribute('data-number'));
+
wrapper.setAttribute('data-id', note.getAttribute('id'));
+
+
// Copy the inner HTML directly to preserve content
+
wrapper.innerHTML = `<span class="sidenote-number">${note.getAttribute('data-number')}.</span><span class="sidenote-content">${note.innerHTML}</span>`;
+
+
container.appendChild(wrapper);
+
});
+
+
// Position sidenotes
+
this.positionSidenotes();
+
}
+
+
// Reposition on resize and scroll
+
let resizeTimeout;
+
const repositionHandler = () => {
+
clearTimeout(resizeTimeout);
+
resizeTimeout = setTimeout(() => {
+
const currentWidth = window.innerWidth;
+
+
if (currentWidth >= minWidthForSidenotes) {
+
// Re-setup if transitioning from mobile to desktop
+
if (container.children.length === 0) {
+
this.setupPositioning();
+
} else {
+
this.positionSidenotes();
+
}
+
} else {
+
// Clear sidenotes container when in mobile view
+
container.innerHTML = '';
+
}
+
}, 150);
+
};
+
+
window.addEventListener('resize', repositionHandler);
+
window.addEventListener('scroll', repositionHandler, { passive: true });
+
+
// Also reposition after all resources load
+
window.addEventListener('load', () => this.positionSidenotes());
+
}
+
+
positionSidenotes() {
+
const minWidthForSidenotes = 980;
+
if (window.innerWidth < minWidthForSidenotes) return;
+
+
const container = this.shadowRoot.getElementById('sidenotes-container');
+
const containerRect = container.getBoundingClientRect();
+
const containerTop = containerRect.top + window.scrollY;
+
+
// Collect reference-sidenote pairs
+
const pairs = [];
+
this.querySelectorAll('sidenote-ref').forEach(ref => {
+
const forId = ref.getAttribute('for');
+
const sidenote = container.querySelector(`.sidenote-wrapper[data-id="${forId}"]`);
+
+
if (sidenote) {
+
const refRect = ref.getBoundingClientRect();
+
const refTop = refRect.top + window.scrollY;
+
+
pairs.push({
+
sidenote,
+
targetTop: refTop - containerTop
+
});
+
}
+
});
+
+
// Sort by position and place with overlap prevention
+
pairs.sort((a, b) => a.targetTop - b.targetTop);
+
+
let lastBottom = 0;
+
pairs.forEach(({ sidenote, targetTop }) => {
+
const adjustedTop = Math.max(targetTop, lastBottom + 20);
+
sidenote.style.top = adjustedTop + 'px';
+
+
const height = sidenote.offsetHeight || 100;
+
lastBottom = adjustedTop + height;
+
});
+
}
+
+
setupHighlighting() {
+
// Setup hover events on wrapper elements in shadow DOM
+
const container = this.shadowRoot.getElementById('sidenotes-container');
+
+
container.addEventListener('mouseenter', (e) => {
+
const wrapper = e.target.closest('.sidenote-wrapper');
+
if (wrapper) {
+
const number = wrapper.getAttribute('data-number');
+
wrapper.classList.add('highlighted');
+
// Highlight corresponding reference
+
const ref = this.querySelector(`sidenote-ref[data-number="${number}"]`);
+
if (ref) ref.highlighted = true;
+
}
+
}, true);
+
+
container.addEventListener('mouseleave', (e) => {
+
const wrapper = e.target.closest('.sidenote-wrapper');
+
if (wrapper) {
+
const number = wrapper.getAttribute('data-number');
+
wrapper.classList.remove('highlighted');
+
// Unhighlight corresponding reference
+
const ref = this.querySelector(`sidenote-ref[data-number="${number}"]`);
+
if (ref) ref.highlighted = false;
+
}
+
}, true);
+
+
// Listen for ref hover events
+
this.addEventListener('ref-hover', (e) => {
+
const { number, active } = e.detail;
+
const wrapper = container.querySelector(`.sidenote-wrapper[data-number="${number}"]`);
+
if (wrapper) {
+
if (active) {
+
wrapper.classList.add('highlighted');
+
} else {
+
wrapper.classList.remove('highlighted');
+
}
+
}
+
});
+
}
+
}
+
+
// ============================================
+
// SIDENOTE-REF - Reference Component
+
// ============================================
+
class SidenoteRef extends HTMLElement {
+
static get observedAttributes() {
+
return ['data-number'];
+
}
+
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
}
+
+
connectedCallback() {
+
this.render();
+
this.setupEvents();
+
}
+
+
attributeChangedCallback(name, oldValue, newValue) {
+
if (name === 'data-number' && oldValue !== newValue) {
+
this.render();
+
}
+
}
+
+
get highlighted() {
+
return this.hasAttribute('highlighted');
+
}
+
+
set highlighted(value) {
+
if (value) {
+
this.setAttribute('highlighted', '');
+
} else {
+
this.removeAttribute('highlighted');
+
}
+
this.updateHighlight();
+
}
+
+
render() {
+
const number = this.getAttribute('data-number') || '';
+
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
display: inline-block;
+
vertical-align: baseline;
+
font-size: 0.85em;
+
font-weight: 500;
+
margin: 0 0.15em;
+
}
+
+
.ref {
+
display: inline-flex;
+
align-items: center;
+
justify-content: center;
+
min-width: 1.4em;
+
height: 1.4em;
+
padding: 0 0.3em;
+
color: var(--accent, #0066cc);
+
background: rgba(0, 102, 204, 0.1);
+
border: 1px solid rgba(0, 102, 204, 0.3);
+
border-radius: 3px;
+
cursor: pointer;
+
text-decoration: none;
+
transition: all 0.2s ease;
+
font-variant-numeric: tabular-nums;
+
}
+
+
.ref:hover,
+
:host([highlighted]) .ref {
+
background: var(--accent, #0066cc);
+
border-color: var(--accent, #0066cc);
+
color: white;
+
transform: scale(1.05);
+
}
+
</style>
+
<span class="ref">${number}</span>
+
`;
+
}
+
+
setupEvents() {
+
this.addEventListener('mouseenter', () => {
+
const number = this.getAttribute('data-number');
+
this.dispatchEvent(new CustomEvent('ref-hover', {
+
detail: { number, active: true },
+
bubbles: true
+
}));
+
});
+
+
this.addEventListener('mouseleave', () => {
+
const number = this.getAttribute('data-number');
+
this.dispatchEvent(new CustomEvent('ref-hover', {
+
detail: { number, active: false },
+
bubbles: true
+
}));
+
});
+
}
+
+
updateHighlight() {
+
// Force re-render to show highlight state
+
const ref = this.shadowRoot.querySelector('.ref');
+
if (ref) {
+
ref.style.background = this.highlighted ? 'var(--accent, #0066cc)' : '';
+
ref.style.color = this.highlighted ? 'white' : '';
+
}
+
}
+
}
+
+
// ============================================
+
// SIDE-NOTE - Note Content Component
+
// ============================================
+
class SideNote extends HTMLElement {
+
static get observedAttributes() {
+
return ['data-number'];
+
}
+
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
}
+
+
connectedCallback() {
+
this.render();
+
}
+
+
attributeChangedCallback(name, oldValue, newValue) {
+
if (name === 'data-number' && oldValue !== newValue) {
+
this.render();
+
}
+
}
+
+
render() {
+
const number = this.getAttribute('data-number') || '';
+
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
display: block;
+
font-size: 0.85rem;
+
line-height: 1.4;
+
color: var(--text-muted, #666);
+
margin: 1rem 0;
+
padding: 0.75rem;
+
background: var(--sidenote-bg, #fafafa);
+
border-left: 3px solid var(--accent, #0066cc);
+
border-radius: 4px;
+
}
+
+
/* Hide on desktop - they're shown in the sidebar */
+
@media (min-width: 980px) {
+
:host {
+
display: none;
+
}
+
}
+
+
.sidenote-mobile-number {
+
font-weight: bold;
+
color: var(--accent, #0066cc);
+
margin-right: 0.5em;
+
}
+
+
/* Mobile/tablet styles */
+
@media (max-width: 979px) {
+
.sidenote-mobile-number {
+
display: inline;
+
}
+
}
+
+
@media (min-width: 980px) {
+
.sidenote-mobile-number {
+
display: none;
+
}
+
}
+
+
::slotted(*) {
+
margin: 0.5rem 0;
+
}
+
+
::slotted(img) {
+
width: 100%;
+
height: auto;
+
border-radius: 4px;
+
}
+
+
::slotted(pre) {
+
background: rgba(0, 0, 0, 0.05);
+
padding: 0.5rem;
+
border-radius: 4px;
+
overflow-x: auto;
+
}
+
</style>
+
${number ? `<span class="sidenote-mobile-number">${number}.</span>` : ''}
+
<slot></slot>
+
`;
+
}
+
}
+
+
// Register components with proper hyphenated names
+
customElements.define('article-sidenotes', ArticleSidenotes);
+
customElements.define('sidenote-ref', SidenoteRef);
+
customElements.define('side-note', SideNote);
+295
arod-web/arod-theme.js
···
+
/**
+
* Arod Theme Toggle Component
+
* A theme switcher for light/dark/system modes
+
*
+
* Usage:
+
* <arod-theme></arod-theme>
+
*
+
* Note: This component manages the theme at the document level
+
* by setting a data-theme attribute on <html> or toggling a class.
+
* It also persists the preference in localStorage.
+
*/
+
+
// ============================================
+
// AROD-THEME - Theme Toggle Component
+
// ============================================
+
class ArodTheme extends HTMLElement {
+
constructor() {
+
super();
+
this.attachShadow({ mode: 'open' });
+
this.themes = ['light', 'dark', 'system'];
+
this.currentTheme = 'system';
+
}
+
+
connectedCallback() {
+
this.loadTheme();
+
this.render();
+
this.setupEventListeners();
+
this.applyTheme();
+
this.watchSystemTheme();
+
}
+
+
disconnectedCallback() {
+
this.unwatchSystemTheme();
+
}
+
+
render() {
+
this.shadowRoot.innerHTML = `
+
<style>
+
:host {
+
display: inline-block;
+
}
+
+
.theme-toggle {
+
position: relative;
+
padding: 0.5rem;
+
background: transparent;
+
border: 1px solid var(--theme-border, #e0e0e0);
+
border-radius: 4px;
+
color: var(--theme-text, #222);
+
cursor: pointer;
+
transition: all 0.2s ease;
+
}
+
+
@media (prefers-color-scheme: dark) {
+
.theme-toggle {
+
--theme-border: #444;
+
--theme-text: #e0e0e0;
+
}
+
}
+
+
.theme-toggle:hover {
+
border-color: var(--theme-accent, #0066cc);
+
background: rgba(0, 102, 204, 0.05);
+
}
+
+
.theme-icon {
+
width: 20px;
+
height: 20px;
+
display: block;
+
}
+
+
.theme-menu {
+
display: none;
+
position: absolute;
+
top: calc(100% + 0.5rem);
+
right: 0;
+
background: var(--theme-menu-bg, #ffffff);
+
border: 1px solid var(--theme-border, #e0e0e0);
+
border-radius: 4px;
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+
overflow: hidden;
+
z-index: 1000;
+
}
+
+
@media (prefers-color-scheme: dark) {
+
.theme-menu {
+
--theme-menu-bg: #2a2a2a;
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+
}
+
}
+
+
.theme-menu.open {
+
display: block;
+
}
+
+
.theme-option {
+
display: flex;
+
align-items: center;
+
gap: 0.75rem;
+
padding: 0.75rem 1rem;
+
background: transparent;
+
border: none;
+
color: var(--theme-text, #222);
+
cursor: pointer;
+
width: 100%;
+
text-align: left;
+
transition: background 0.2s ease;
+
}
+
+
.theme-option:hover {
+
background: rgba(0, 102, 204, 0.1);
+
}
+
+
.theme-option.active {
+
background: rgba(0, 102, 204, 0.15);
+
color: var(--theme-accent, #0066cc);
+
}
+
+
.theme-label {
+
min-width: 4rem;
+
}
+
+
/* Icon animations */
+
.theme-icon svg {
+
transition: transform 0.3s ease;
+
}
+
+
.theme-toggle:hover .theme-icon svg {
+
transform: rotate(20deg);
+
}
+
+
/* Dark mode overrides when active */
+
:host([data-theme="dark"]) .theme-toggle {
+
--theme-border: #444;
+
--theme-text: #e0e0e0;
+
--theme-menu-bg: #2a2a2a;
+
}
+
+
:host([data-theme="light"]) .theme-toggle {
+
--theme-border: #e0e0e0;
+
--theme-text: #222;
+
--theme-menu-bg: #ffffff;
+
}
+
</style>
+
+
<button class="theme-toggle" aria-label="Toggle theme" aria-expanded="false">
+
<span class="theme-icon"></span>
+
</button>
+
+
<div class="theme-menu" role="menu">
+
<button class="theme-option" data-theme="light" role="menuitem">
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
+
</svg>
+
<span class="theme-label">Light</span>
+
</button>
+
<button class="theme-option" data-theme="dark" role="menuitem">
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
+
</svg>
+
<span class="theme-label">Dark</span>
+
</button>
+
<button class="theme-option" data-theme="system" role="menuitem">
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
+
</svg>
+
<span class="theme-label">System</span>
+
</button>
+
</div>
+
`;
+
}
+
+
setupEventListeners() {
+
const toggle = this.shadowRoot.querySelector('.theme-toggle');
+
const menu = this.shadowRoot.querySelector('.theme-menu');
+
const options = this.shadowRoot.querySelectorAll('.theme-option');
+
+
toggle.addEventListener('click', () => {
+
const isOpen = menu.classList.contains('open');
+
menu.classList.toggle('open');
+
toggle.setAttribute('aria-expanded', !isOpen);
+
});
+
+
// Close menu when clicking outside
+
document.addEventListener('click', (e) => {
+
if (!this.contains(e.target)) {
+
menu.classList.remove('open');
+
toggle.setAttribute('aria-expanded', 'false');
+
}
+
});
+
+
options.forEach(option => {
+
option.addEventListener('click', () => {
+
const theme = option.dataset.theme;
+
this.setTheme(theme);
+
menu.classList.remove('open');
+
toggle.setAttribute('aria-expanded', 'false');
+
});
+
});
+
}
+
+
loadTheme() {
+
// Load from localStorage or default to system
+
const saved = localStorage.getItem('arod-theme');
+
this.currentTheme = saved || 'system';
+
}
+
+
setTheme(theme) {
+
this.currentTheme = theme;
+
localStorage.setItem('arod-theme', theme);
+
this.applyTheme();
+
this.updateUI();
+
+
// Dispatch event for other components to react
+
this.dispatchEvent(new CustomEvent('theme-change', {
+
detail: { theme },
+
bubbles: true,
+
composed: true
+
}));
+
}
+
+
applyTheme() {
+
const root = document.documentElement;
+
let effectiveTheme = this.currentTheme;
+
+
if (this.currentTheme === 'system') {
+
effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+
}
+
+
// Set data attribute on html element
+
root.setAttribute('data-theme', effectiveTheme);
+
+
// Also set on this component for styling
+
this.setAttribute('data-theme', effectiveTheme);
+
+
// Update all arod components
+
document.querySelectorAll('arod-menu, article-sidenotes').forEach(el => {
+
el.setAttribute('theme', effectiveTheme);
+
});
+
+
this.updateUI();
+
}
+
+
updateUI() {
+
const icon = this.shadowRoot.querySelector('.theme-icon');
+
const options = this.shadowRoot.querySelectorAll('.theme-option');
+
+
// Update active state
+
options.forEach(option => {
+
option.classList.toggle('active', option.dataset.theme === this.currentTheme);
+
});
+
+
// Update icon based on current theme
+
let iconSvg;
+
const effectiveTheme = this.getEffectiveTheme();
+
+
if (effectiveTheme === 'dark') {
+
iconSvg = `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
+
</svg>`;
+
} else {
+
iconSvg = `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
+
</svg>`;
+
}
+
+
icon.innerHTML = iconSvg;
+
}
+
+
getEffectiveTheme() {
+
if (this.currentTheme === 'system') {
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+
}
+
return this.currentTheme;
+
}
+
+
watchSystemTheme() {
+
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+
this.systemThemeHandler = () => {
+
if (this.currentTheme === 'system') {
+
this.applyTheme();
+
}
+
};
+
this.mediaQuery.addEventListener('change', this.systemThemeHandler);
+
}
+
+
unwatchSystemTheme() {
+
if (this.mediaQuery && this.systemThemeHandler) {
+
this.mediaQuery.removeEventListener('change', this.systemThemeHandler);
+
}
+
}
+
}
+
+
// Register component
+
customElements.define('arod-theme', ArodTheme);
+326
arod-web/example.html
···
+
<!DOCTYPE html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8">
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
+
<title>Arod Web Components - Full Example</title>
+
+
<!-- Import all Arod components -->
+
<script src="arod-menu.js"></script>
+
<script src="arod-search.js"></script>
+
<script src="arod-theme.js"></script>
+
<script src="arod-sidenotes.js"></script>
+
+
<style>
+
/* Base styles that respond to theme */
+
:root {
+
--bg: #ffffff;
+
--text: #222;
+
--text-muted: #666;
+
--accent: #0066cc;
+
--border: #e0e0e0;
+
}
+
+
[data-theme="dark"] {
+
--bg: #1a1a1a;
+
--text: #e0e0e0;
+
--text-muted: #999;
+
--accent: #4db8ff;
+
--border: #444;
+
}
+
+
* {
+
margin: 0;
+
padding: 0;
+
box-sizing: border-box;
+
}
+
+
body {
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+
line-height: 1.6;
+
color: var(--text);
+
background: var(--bg);
+
transition: background 0.3s ease, color 0.3s ease;
+
}
+
+
/* Main content area */
+
main {
+
padding: 2rem 1rem;
+
max-width: 980px;
+
margin: 0 auto;
+
}
+
+
h1 {
+
font-size: 2.5rem;
+
margin: 2rem 0;
+
}
+
+
h2 {
+
font-size: 1.75rem;
+
margin: 2rem 0 1rem;
+
}
+
+
p {
+
margin-bottom: 1.25rem;
+
text-align: justify;
+
}
+
+
code {
+
font-family: 'SF Mono', Consolas, monospace;
+
font-size: 0.9em;
+
padding: 0.2em 0.4em;
+
background: var(--border);
+
border-radius: 3px;
+
}
+
+
pre {
+
padding: 1rem;
+
margin: 1.5rem 0;
+
background: var(--border);
+
border-radius: 4px;
+
overflow-x: auto;
+
}
+
+
pre code {
+
background: none;
+
padding: 0;
+
}
+
+
blockquote {
+
margin: 1.5rem 0;
+
padding-left: 1rem;
+
border-left: 3px solid var(--accent);
+
font-style: italic;
+
color: var(--text-muted);
+
}
+
+
.hero {
+
text-align: center;
+
padding: 4rem 0;
+
background: linear-gradient(135deg, rgba(0,102,204,0.1) 0%, rgba(0,102,204,0.05) 100%);
+
border-radius: 8px;
+
margin: 2rem 0;
+
}
+
+
.hero h1 {
+
margin-bottom: 1rem;
+
}
+
+
.hero p {
+
font-size: 1.25rem;
+
color: var(--text-muted);
+
text-align: center;
+
}
+
</style>
+
</head>
+
<body>
+
<!-- Navigation Menu -->
+
<arod-menu>
+
<menu-item href="#home">Home</menu-item>
+
<menu-item href="#features">Features</menu-item>
+
<menu-item href="#docs">Documentation</menu-item>
+
<menu-item href="#about">About</menu-item>
+
+
<!-- Actions slot for search and theme toggle -->
+
<arod-search slot="actions" placeholder="Search documentation..."></arod-search>
+
<arod-theme slot="actions"></arod-theme>
+
</arod-menu>
+
+
<main>
+
<div class="hero">
+
<h1>Arod Web Components</h1>
+
<p>Modern, accessible web components for content-rich websites</p>
+
</div>
+
+
<article-sidenotes>
+
<h1>Welcome to Arod Components</h1>
+
+
<p>
+
The Arod web component library provides a set of semantic, accessible
+
components<sidenote-ref for="note1"></sidenote-ref> for building
+
modern documentation sites and content-rich applications. Each component
+
is designed to work independently<sidenote-ref for="note2"></sidenote-ref>
+
while maintaining consistent styling and behavior.
+
</p>
+
+
<side-note id="note1">
+
Web Components are a set of web platform APIs that allow you to create
+
custom, reusable HTML elements with encapsulated functionality.
+
</side-note>
+
+
<side-note id="note2">
+
Components use Shadow DOM for style encapsulation, ensuring they don't
+
interfere with your existing styles.
+
</side-note>
+
+
<h2>Key Features</h2>
+
+
<p>
+
The library includes essential components like a responsive navigation
+
menu<sidenote-ref for="note3"></sidenote-ref>, an advanced search
+
interface<sidenote-ref for="note4"></sidenote-ref>, theme switching
+
capabilities<sidenote-ref for="note5"></sidenote-ref>, and our unique
+
sidenotes system<sidenote-ref for="note6"></sidenote-ref> for rich
+
annotations.
+
</p>
+
+
<side-note id="note3">
+
The menu component automatically adapts to mobile devices with a
+
hamburger menu and supports marking active items.
+
</side-note>
+
+
<side-note id="note4">
+
The search component provides keyboard shortcuts (โŒ˜K), real-time
+
search, and keyboard navigation through results.
+
</side-note>
+
+
<side-note id="note5">
+
Theme switching persists user preference in localStorage and respects
+
system preferences when set to "System" mode.
+
</side-note>
+
+
<side-note id="note6">
+
Sidenotes appear in the margin on desktop and inline on mobile,
+
with automatic numbering and bidirectional hover highlighting.
+
</side-note>
+
+
<h2>Installation</h2>
+
+
<p>
+
Simply include the component scripts in your HTML and start using
+
the custom elements<sidenote-ref for="note7"></sidenote-ref>. No build
+
process or framework required!
+
</p>
+
+
<side-note id="note7">
+
<pre><code>&lt;script src="arod/arod-menu.js"&gt;&lt;/script&gt;
+
&lt;script src="arod/arod-search.js"&gt;&lt;/script&gt;
+
&lt;script src="arod/arod-theme.js"&gt;&lt;/script&gt;
+
&lt;script src="arod/arod-sidenotes.js"&gt;&lt;/script&gt;</code></pre>
+
</side-note>
+
+
<h2>Component Examples</h2>
+
+
<h3>Navigation Menu</h3>
+
+
<pre><code>&lt;arod-menu&gt;
+
&lt;menu-item href="/papers"&gt;Papers&lt;/menu-item&gt;
+
&lt;menu-item href="/notes"&gt;Notes&lt;/menu-item&gt;
+
&lt;menu-item href="/about"&gt;About&lt;/menu-item&gt;
+
+
&lt;arod-search slot="actions"&gt;&lt;/arod-search&gt;
+
&lt;arod-theme slot="actions"&gt;&lt;/arod-theme&gt;
+
&lt;/arod-menu&gt;</code></pre>
+
+
<h3>Sidenotes</h3>
+
+
<pre><code>&lt;article-sidenotes&gt;
+
&lt;p&gt;
+
Text with a reference&lt;sidenote-ref for="note1"&gt;&lt;/sidenote-ref&gt;
+
&lt;/p&gt;
+
+
&lt;side-note id="note1"&gt;
+
The sidenote content goes here
+
&lt;/side-note&gt;
+
&lt;/article-sidenotes&gt;</code></pre>
+
+
<h3>Search Integration</h3>
+
+
<p>
+
The search component emits events<sidenote-ref for="note8"></sidenote-ref>
+
that you can handle to integrate with your search backend:
+
</p>
+
+
<side-note id="note8">
+
<pre><code>const search = document.querySelector('arod-search');
+
search.addEventListener('search', async (e) => {
+
const results = await searchAPI(e.detail.query);
+
search.displayResults(results);
+
});</code></pre>
+
</side-note>
+
+
<h2>Theming</h2>
+
+
<p>
+
All components support both light and dark themes<sidenote-ref for="note9"></sidenote-ref>.
+
The theme toggle component manages this at the document level by setting
+
a <code>data-theme</code> attribute on the HTML element.
+
</p>
+
+
<side-note id="note9">
+
Components use CSS custom properties for theming, making it easy to
+
customize colors and styles to match your brand.
+
</side-note>
+
+
<blockquote>
+
"Good design is as little design as possible." โ€” Dieter Rams
+
</blockquote>
+
+
<h2>Browser Support</h2>
+
+
<p>
+
Arod components work in all modern browsers<sidenote-ref for="note10"></sidenote-ref>
+
that support Web Components APIs. For older browsers, consider using
+
polyfills.
+
</p>
+
+
<side-note id="note10">
+
Tested in Chrome 90+, Firefox 88+, Safari 14+, and Edge 90+.
+
Internet Explorer is not supported.
+
</side-note>
+
+
<h2>Contributing</h2>
+
+
<p>
+
We welcome contributions! The library is designed to be extensible<sidenote-ref for="note11"></sidenote-ref>,
+
and new components that follow the established patterns are encouraged.
+
</p>
+
+
<side-note id="note11">
+
Each component should be self-contained, use Shadow DOM for encapsulation,
+
emit appropriate events, and follow the naming convention of
+
<code>arod-[component]</code>.
+
</side-note>
+
</article-sidenotes>
+
</main>
+
+
<script>
+
// Example search integration
+
const searchComponent = document.querySelector('arod-search');
+
+
searchComponent.addEventListener('search', (e) => {
+
console.log('Search query:', e.detail.query);
+
+
// Mock search results for demo
+
setTimeout(() => {
+
const mockResults = [
+
{
+
title: 'Getting Started with Arod',
+
snippet: 'Learn how to install and use <mark>Arod</mark> components...',
+
url: '#docs'
+
},
+
{
+
title: 'Sidenotes Component',
+
snippet: 'The <mark>sidenotes</mark> component provides margin notes...',
+
url: '#sidenotes'
+
},
+
{
+
title: 'Theme Switching',
+
snippet: 'Support for light, dark, and system <mark>themes</mark>...',
+
url: '#themes'
+
}
+
];
+
+
if (e.detail.query) {
+
searchComponent.displayResults(mockResults);
+
}
+
}, 300);
+
});
+
+
// Log theme changes
+
document.querySelector('arod-theme').addEventListener('theme-change', (e) => {
+
console.log('Theme changed to:', e.detail.theme);
+
});
+
</script>
+
</body>
+
</html>