mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 17:47:16 +00:00
style: format all files with prettier
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
### Modal Dialog
|
||||
|
||||
```tsx
|
||||
import { useEffect, useRef, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -23,27 +23,27 @@ export function Modal({ isOpen, onClose, title, children }: ModalProps) {
|
||||
if (isOpen) {
|
||||
previousActiveElement.current = document.activeElement;
|
||||
dialogRef.current?.focus();
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.overflow = "";
|
||||
(previousActiveElement.current as HTMLElement)?.focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if (e.key === 'Tab') trapFocus(e, dialogRef.current);
|
||||
if (e.key === "Escape") onClose();
|
||||
if (e.key === "Tab") trapFocus(e, dialogRef.current);
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
}
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
@@ -82,7 +82,7 @@ export function Modal({ isOpen, onClose, title, children }: ModalProps) {
|
||||
<div className="mt-4">{children}</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ function trapFocus(e: KeyboardEvent, container: HTMLElement | null) {
|
||||
if (!container) return;
|
||||
|
||||
const focusableElements = container.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
@@ -108,7 +108,7 @@ function trapFocus(e: KeyboardEvent, container: HTMLElement | null) {
|
||||
### Dropdown Menu
|
||||
|
||||
```tsx
|
||||
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
||||
import { useState, useRef, useEffect, type ReactNode } from "react";
|
||||
|
||||
interface DropdownProps {
|
||||
trigger: ReactNode;
|
||||
@@ -124,22 +124,25 @@ export function Dropdown({ trigger, children, label }: DropdownProps) {
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
case "Escape":
|
||||
setIsOpen(false);
|
||||
triggerRef.current?.focus();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
@@ -147,17 +150,17 @@ export function Dropdown({ trigger, children, label }: DropdownProps) {
|
||||
focusNextItem(menuRef.current, 1);
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
if (isOpen) {
|
||||
focusNextItem(menuRef.current, -1);
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
case "Home":
|
||||
e.preventDefault();
|
||||
focusFirstItem(menuRef.current);
|
||||
break;
|
||||
case 'End':
|
||||
case "End":
|
||||
e.preventDefault();
|
||||
focusLastItem(menuRef.current);
|
||||
break;
|
||||
@@ -177,7 +180,7 @@ export function Dropdown({ trigger, children, label }: DropdownProps) {
|
||||
{trigger}
|
||||
<ChevronDownIcon
|
||||
aria-hidden="true"
|
||||
className={`transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
className={`transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -217,18 +220,26 @@ export function MenuItem({ children, onClick, disabled }: MenuItemProps) {
|
||||
|
||||
function focusNextItem(menu: HTMLElement | null, direction: 1 | -1) {
|
||||
if (!menu) return;
|
||||
const items = menu.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
|
||||
const currentIndex = Array.from(items).indexOf(document.activeElement as HTMLElement);
|
||||
const items = menu.querySelectorAll<HTMLElement>(
|
||||
'[role="menuitem"]:not([disabled])',
|
||||
);
|
||||
const currentIndex = Array.from(items).indexOf(
|
||||
document.activeElement as HTMLElement,
|
||||
);
|
||||
const nextIndex = (currentIndex + direction + items.length) % items.length;
|
||||
items[nextIndex]?.focus();
|
||||
}
|
||||
|
||||
function focusFirstItem(menu: HTMLElement | null) {
|
||||
menu?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus();
|
||||
menu
|
||||
?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')
|
||||
?.focus();
|
||||
}
|
||||
|
||||
function focusLastItem(menu: HTMLElement | null) {
|
||||
const items = menu?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
|
||||
const items = menu?.querySelectorAll<HTMLElement>(
|
||||
'[role="menuitem"]:not([disabled])',
|
||||
);
|
||||
items?.[items.length - 1]?.focus();
|
||||
}
|
||||
```
|
||||
@@ -236,7 +247,13 @@ function focusLastItem(menu: HTMLElement | null) {
|
||||
### Combobox / Autocomplete
|
||||
|
||||
```tsx
|
||||
import { useState, useRef, useId, type ChangeEvent, type KeyboardEvent } from 'react';
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useId,
|
||||
type ChangeEvent,
|
||||
type KeyboardEvent,
|
||||
} from "react";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
@@ -259,7 +276,7 @@ export function Combobox({
|
||||
placeholder,
|
||||
}: ComboboxProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [activeIndex, setActiveIndex] = useState(-1);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -268,7 +285,7 @@ export function Combobox({
|
||||
const listboxId = useId();
|
||||
|
||||
const filteredOptions = options.filter((option) =>
|
||||
option.label.toLowerCase().includes(inputValue.toLowerCase())
|
||||
option.label.toLowerCase().includes(inputValue.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -286,27 +303,27 @@ export function Combobox({
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
setActiveIndex((prev) =>
|
||||
prev < filteredOptions.length - 1 ? prev + 1 : prev
|
||||
prev < filteredOptions.length - 1 ? prev + 1 : prev,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
||||
break;
|
||||
case 'Enter':
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
if (activeIndex >= 0 && filteredOptions[activeIndex]) {
|
||||
handleSelect(filteredOptions[activeIndex]);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
case "Escape":
|
||||
setIsOpen(false);
|
||||
break;
|
||||
}
|
||||
@@ -353,8 +370,8 @@ export function Combobox({
|
||||
aria-selected={activeIndex === index}
|
||||
onClick={() => handleSelect(option)}
|
||||
className={`cursor-pointer px-3 py-2 ${
|
||||
activeIndex === index ? 'bg-blue-100' : 'hover:bg-gray-100'
|
||||
} ${value === option.value ? 'font-medium' : ''}`}
|
||||
activeIndex === index ? "bg-blue-100" : "hover:bg-gray-100"
|
||||
} ${value === option.value ? "font-medium" : ""}`}
|
||||
>
|
||||
{option.label}
|
||||
</li>
|
||||
@@ -375,7 +392,7 @@ export function Combobox({
|
||||
### Form Validation
|
||||
|
||||
```tsx
|
||||
import { useId, type FormEvent } from 'react';
|
||||
import { useId, type FormEvent } from "react";
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string;
|
||||
@@ -383,12 +400,17 @@ interface FormFieldProps {
|
||||
required?: boolean;
|
||||
children: (props: {
|
||||
id: string;
|
||||
'aria-describedby': string | undefined;
|
||||
'aria-invalid': boolean;
|
||||
"aria-describedby": string | undefined;
|
||||
"aria-invalid": boolean;
|
||||
}) => ReactNode;
|
||||
}
|
||||
|
||||
export function FormField({ label, error, required, children }: FormFieldProps) {
|
||||
export function FormField({
|
||||
label,
|
||||
error,
|
||||
required,
|
||||
children,
|
||||
}: FormFieldProps) {
|
||||
const id = useId();
|
||||
const errorId = `${id}-error`;
|
||||
|
||||
@@ -405,8 +427,8 @@ export function FormField({ label, error, required, children }: FormFieldProps)
|
||||
|
||||
{children({
|
||||
id,
|
||||
'aria-describedby': error ? errorId : undefined,
|
||||
'aria-invalid': !!error,
|
||||
"aria-describedby": error ? errorId : undefined,
|
||||
"aria-invalid": !!error,
|
||||
})}
|
||||
|
||||
{error && (
|
||||
@@ -436,13 +458,16 @@ function ContactForm() {
|
||||
type="email"
|
||||
required
|
||||
className={`w-full rounded border px-3 py-2 ${
|
||||
props['aria-invalid'] ? 'border-red-500' : 'border-gray-300'
|
||||
props["aria-invalid"] ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<button type="submit" className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
@@ -476,19 +501,22 @@ export function SkipLinks() {
|
||||
## Live Regions
|
||||
|
||||
```tsx
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface LiveAnnouncerProps {
|
||||
message: string;
|
||||
politeness?: 'polite' | 'assertive';
|
||||
politeness?: "polite" | "assertive";
|
||||
}
|
||||
|
||||
export function LiveAnnouncer({ message, politeness = 'polite' }: LiveAnnouncerProps) {
|
||||
const [announcement, setAnnouncement] = useState('');
|
||||
export function LiveAnnouncer({
|
||||
message,
|
||||
politeness = "polite",
|
||||
}: LiveAnnouncerProps) {
|
||||
const [announcement, setAnnouncement] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// Clear first, then set - ensures screen readers pick up the change
|
||||
setAnnouncement('');
|
||||
setAnnouncement("");
|
||||
const timer = setTimeout(() => setAnnouncement(message), 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [message]);
|
||||
@@ -506,9 +534,15 @@ export function LiveAnnouncer({ message, politeness = 'polite' }: LiveAnnouncerP
|
||||
}
|
||||
|
||||
// Usage in a search component
|
||||
function SearchResults({ results, loading }: { results: Item[]; loading: boolean }) {
|
||||
function SearchResults({
|
||||
results,
|
||||
loading,
|
||||
}: {
|
||||
results: Item[];
|
||||
loading: boolean;
|
||||
}) {
|
||||
const message = loading
|
||||
? 'Loading results...'
|
||||
? "Loading results..."
|
||||
: `${results.length} results found`;
|
||||
|
||||
return (
|
||||
@@ -544,12 +578,14 @@ function useFocusTrap(containerRef: RefObject<HTMLElement>, isActive: boolean) {
|
||||
if (!isActive || !containerRef.current) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
const focusableSelector =
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Tab') return;
|
||||
if (e.key !== "Tab") return;
|
||||
|
||||
const focusableElements = container.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
const focusableElements =
|
||||
container.querySelectorAll<HTMLElement>(focusableSelector);
|
||||
const first = focusableElements[0];
|
||||
const last = focusableElements[focusableElements.length - 1];
|
||||
|
||||
@@ -562,8 +598,8 @@ function useFocusTrap(containerRef: RefObject<HTMLElement>, isActive: boolean) {
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener('keydown', handleKeyDown);
|
||||
return () => container.removeEventListener('keydown', handleKeyDown);
|
||||
container.addEventListener("keydown", handleKeyDown);
|
||||
return () => container.removeEventListener("keydown", handleKeyDown);
|
||||
}, [containerRef, isActive]);
|
||||
}
|
||||
```
|
||||
@@ -595,8 +631,12 @@ function getContrastRatio(fg: string, bg: string): number {
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
function meetsWCAG(fg: string, bg: string, level: 'AA' | 'AAA' = 'AA'): boolean {
|
||||
function meetsWCAG(
|
||||
fg: string,
|
||||
bg: string,
|
||||
level: "AA" | "AAA" = "AA",
|
||||
): boolean {
|
||||
const ratio = getContrastRatio(fg, bg);
|
||||
return level === 'AAA' ? ratio >= 7 : ratio >= 4.5;
|
||||
return level === "AAA" ? ratio >= 7 : ratio >= 4.5;
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user