UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

139 lines 5.55 kB
import { Shade, createComponent } from '@furystack/shades'; import { buildTransition, cssVariableTheme } from '../../services/css-variable-theme.js'; import { collapse, expand } from '../animations.js'; import { Icon } from '../icons/icon.js'; import { chevronDown } from '../icons/icon-definitions.js'; /** * An individual collapsible section for use within an Accordion container. * * Supports animated expand/collapse, keyboard accessibility, and optional icon. * * @example * ```tsx * <AccordionItem title="Section 1" defaultExpanded> * <Typography variant="body1">Content goes here</Typography> * </AccordionItem> * ``` */ export const AccordionItem = Shade({ customElementName: 'shade-accordion-item', css: { display: 'block', fontFamily: cssVariableTheme.typography.fontFamily, '&:not(:last-child)': { borderBottom: `1px solid color-mix(in srgb, ${cssVariableTheme.text.secondary} 20%, transparent)`, }, // Header '& .accordion-header': { display: 'flex', alignItems: 'center', gap: cssVariableTheme.spacing.sm, padding: `${cssVariableTheme.spacing.md} ${cssVariableTheme.spacing.lg}`, cursor: 'pointer', userSelect: 'none', backgroundColor: 'transparent', border: 'none', width: '100%', textAlign: 'left', outline: 'none', font: 'inherit', color: 'inherit', transition: buildTransition([ 'background-color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default, ]), }, '& .accordion-header:hover:not(:disabled)': { backgroundColor: `color-mix(in srgb, ${cssVariableTheme.text.secondary} 8%, transparent)`, }, '& .accordion-header:focus-visible': { outline: 'none', boxShadow: cssVariableTheme.action.focusRing, }, // Icon '& .accordion-icon': { display: 'inline-flex', alignItems: 'center', flexShrink: '0', fontSize: '1.2em', }, // Title '& .accordion-title': { flex: '1', fontSize: cssVariableTheme.typography.fontSize.md, fontWeight: cssVariableTheme.typography.fontWeight.medium, color: cssVariableTheme.text.primary, }, // Chevron '& .accordion-chevron': { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: '0', fontSize: '1.2em', color: cssVariableTheme.text.secondary, transform: 'rotate(-90deg)', transition: buildTransition([ 'transform', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default, ]), }, '&[data-expanded] .accordion-chevron': { transform: 'rotate(0deg)', }, // Content wrapper (animated) '& .accordion-content': { overflow: 'hidden', }, // Content inner (provides padding) '& .accordion-content-inner': { padding: `0 ${cssVariableTheme.spacing.lg} ${cssVariableTheme.spacing.lg}`, }, // Disabled state '&[data-disabled]': { opacity: cssVariableTheme.action.disabledOpacity, pointerEvents: 'none', }, '&[data-disabled] .accordion-header': { cursor: 'not-allowed', }, }, render: ({ props, children, useHostProps, useRef, useState }) => { const [isExpanded, setIsExpanded] = useState('expanded', !!props.defaultExpanded); useHostProps({ 'data-disabled': props.disabled ? '' : undefined, 'data-expanded': isExpanded ? '' : undefined, }); const contentRef = useRef('content'); const handleToggle = async () => { if (props.disabled) return; const content = contentRef.current; if (!content) return; if (isExpanded) { setIsExpanded(false); await collapse(content, { duration: 250 }); } else { content.inert = false; setIsExpanded(true); await expand(content, { duration: 250 }); } }; return (createComponent(createComponent, null, createComponent("button", { className: "accordion-header", type: "button", disabled: props.disabled, onclick: handleToggle, "aria-expanded": String(isExpanded) }, props.icon ? createComponent("span", { className: "accordion-icon" }, props.icon) : null, createComponent("span", { className: "accordion-title" }, props.title), createComponent("span", { className: "accordion-chevron" }, createComponent(Icon, { icon: chevronDown, size: 16 }))), createComponent("div", { ref: contentRef, className: "accordion-content", inert: !isExpanded, style: { height: isExpanded ? undefined : '0px', opacity: isExpanded ? '1' : '0', } }, createComponent("div", { className: "accordion-content-inner" }, children)))); }, }); //# sourceMappingURL=accordion-item.js.map