Files
dt-design-system/packages/react/src/components/DTAccordion.tsx
michael e74c285b70 Add storefront component migration: cards, badges, progress bars, animations, and React wrapper package
Major feature migration from dt-shopify-storefront into the design system:

- Card system: per-card color modes (normal/emphasis/warning/success/other),
  progress bars, full-bleed card body (card-body-flush), image bevel clip-paths,
  and visible structural borders at bevel corners
- Badge system: dt-card-badge with position-aware offsets for cards vs media frames
- Media frames: dt-bevel-media with inner surface fill, image clipping, and
  "MISSING MEDIA" placeholder for empty containers
- Interactive bevel buttons: dt-btn with hover/selected states and pulse animation
- Menu items: dt-menu-item with active states and level indentation
- Animations: scale-in, fade-in, slide-up, pulse, ping, spin keyframes
- Feature legend: CSS grid for product feature icons with state colors
- Scrollbar styling for DT brand
- New @dangerousthings/react package wrapping web CSS components
- New @dangerousthings/tailwind-preset package
- New @dangerousthings/hex-background package
- Desktop showcase rewritten in React with Tailwind CSS
- Mobile showcase updated with new screens (animations, filters, advanced cards)
- Tokens: added mode color tokens, RGB variants, and selected-state tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:18:39 -08:00

84 lines
2.2 KiB
TypeScript

/**
* DTAccordion — Expandable sections with animated height and chevron.
*
* CSS reference: forms-dt.css .dt-accordion, .dt-accordion-header,
* .dt-accordion-chevron, .dt-accordion-content
*/
import { useState, useCallback, type ReactNode, type CSSProperties } from 'react';
import type { DTVariant } from '@dangerousthings/tokens';
import { cx } from '../utils/cx';
export interface DTAccordionSection {
key: string;
title: string;
children: ReactNode;
}
interface DTAccordionProps {
sections: DTAccordionSection[];
/** Header variant when closed @default 'normal' */
variant?: DTVariant;
/** Header variant when open @default 'emphasis' */
activeVariant?: DTVariant;
/** Allow multiple sections open @default false */
allowMultiple?: boolean;
initialOpenKeys?: string[];
onSectionToggle?: (key: string, isOpen: boolean) => void;
className?: string;
style?: CSSProperties;
}
export function DTAccordion({
sections,
allowMultiple = false,
initialOpenKeys,
onSectionToggle,
className,
style,
}: DTAccordionProps) {
const [openKeys, setOpenKeys] = useState<Set<string>>(
new Set(initialOpenKeys),
);
const toggle = useCallback(
(key: string) => {
setOpenKeys(prev => {
const next = new Set(allowMultiple ? prev : []);
const isOpening = !prev.has(key);
if (isOpening) next.add(key);
else next.delete(key);
onSectionToggle?.(key, isOpening);
return next;
});
},
[allowMultiple, onSectionToggle],
);
return (
<div className={cx('dt-accordion', className)} style={style}>
{sections.map(section => {
const isOpen = openKeys.has(section.key);
return (
<div key={section.key}>
<button
className="dt-accordion-header"
aria-expanded={isOpen}
onClick={() => toggle(section.key)}
type="button"
style={{ width: '100%' }}>
<span>{section.title}</span>
<span className="dt-accordion-chevron" />
</button>
<div
className="dt-accordion-content"
data-open={isOpen}>
{section.children}
</div>
</div>
);
})}
</div>
);
}