UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

417 lines (414 loc) 13.8 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { forwardRef, useState, useCallback, createContext, useContext } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreference } from '../../hooks/useMotionPreference.js'; import { useGlassSound } from '../../utils/soundDesign.js'; const DEFAULT_TREE_DATA = [{ id: "overview", label: "Overview", children: [{ id: "analytics", label: "Analytics" }, { id: "performance", label: "Performance", children: [{ id: "lighthouse", label: "Lighthouse" }] }] }, { id: "settings", label: "Settings", children: [{ id: "appearance", label: "Appearance" }, { id: "tokens", label: "Design Tokens" }] }]; const TreeContext = /*#__PURE__*/createContext(null); const useTreeContext = () => { const context = useContext(TreeContext); if (!context) { throw new Error("Tree components must be used within GlassTreeView"); } return context; }; const GlassTreeNodeComponent = ({ node, level }) => { const { shouldAnimate } = useMotionPreference(); const { play } = useGlassSound(); const [isDragOver, setIsDragOver] = useState(null); const nodeId = useA11yId("tree-node"); const { selectedId, selectedIds, expandedIds, selectionMode, showLines, showIcons, showCheckboxes, draggable, size, variant, indentation, expandIcon, collapseIcon, renderNode, respectMotionPreference, onNodeSelect, onNodeToggle, onNodeCheck, onDragStart, onDrop } = useTreeContext(); const isExpanded = expandedIds.includes(node.id); const isSelected = selectionMode === "single" ? selectedId === node.id : selectedIds.includes(node.id); const hasChildren = node.children && node.children.length > 0; const isChecked = selectedIds.includes(node.id); const sizeConfig = { sm: { node: "h-8 glass-px-2 glass-text-sm", icon: "w-4 h-4", checkbox: "w-3 h-3", expandIcon: "w-3 h-3" }, md: { node: "h-10 glass-px-3 glass-text-sm", icon: "w-5 h-5", checkbox: "w-4 h-4", expandIcon: "w-4 h-4" }, lg: { node: "h-12 glass-px-4 glass-text-base", icon: "w-6 h-6", checkbox: "w-5 h-5", expandIcon: "w-5 h-5" } }; const variantConfig = { default: { node: "hover:bg-background/20 focus:bg-background/30", selected: "bg-primary/20 text-primary", dragOver: "bg-primary/10 border-primary/30" }, minimal: { node: "hover:bg-transparent focus:bg-transparent hover:text-foreground", selected: "text-primary font-medium", dragOver: "bg-primary/5" }, bordered: { node: "border border-transparent hover:border-border/20 focus:border-primary/30", selected: "border-primary/50 bg-primary/10 text-primary", dragOver: "border-primary/50 bg-primary/5" }, filled: { node: "bg-background/10 hover:bg-background/20 focus:bg-background/30", selected: "bg-primary/20 text-primary", dragOver: "bg-primary/15 border-primary/30" } }; const config = sizeConfig[size]; const colors = variantConfig[variant]; const handleNodeClick = () => { if (node.disabled) return; onNodeSelect(node.id, node); play("select"); }; const handleToggle = e => { e.stopPropagation(); if (hasChildren && !node.disabled) { onNodeToggle(node.id); play(isExpanded ? "collapse" : "expand"); } }; const handleCheck = e => { e.stopPropagation(); if (!node.disabled) { onNodeCheck(node.id, e.target.checked); play(e.target.checked ? "check" : "uncheck"); } }; const handleDragStart = e => { if (!draggable || node.disabled) return; e.dataTransfer.setData("text/plain", node.id); onDragStart?.(node, e); }; const handleDragOver = e => { if (!draggable) return; e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); const y = e.clientY - rect.top; const height = rect.height; if (y < height * 0.25) { setIsDragOver("before"); } else if (y > height * 0.75) { setIsDragOver("after"); } else { setIsDragOver("inside"); } }; const handleDragLeave = () => { setIsDragOver(null); }; const handleDrop = e => { if (!draggable) return; e.preventDefault(); const draggedNodeId = e.dataTransfer.getData("text/plain"); // Find dragged node (would need to be passed through context or prop) // This is a simplified version const draggedNode = { id: draggedNodeId }; if (isDragOver && onDrop) { onDrop(draggedNode, node, isDragOver); } setIsDragOver(null); }; const defaultExpandIcon = jsx("svg", { className: 'glass-w-full glass-h-full transition-transform duration-200', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) }); const defaultCollapseIcon = jsx("svg", { className: 'glass-w-full glass-h-full transition-transform duration-200 rotate-90', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) }); if (renderNode) { return jsxs("div", { "data-glass-component": true, children: [renderNode(node, level, isSelected, isExpanded), hasChildren && isExpanded && jsx(MotionFramer, { preset: shouldAnimate && respectMotionPreference ? "slideDown" : "none", className: 'relative', children: node.children?.map(child => jsx(GlassTreeNodeComponent, { node: child, level: level + 1 }, child.id)) })] }); } return jsx(MotionFramer, { preset: shouldAnimate && respectMotionPreference ? "slideIn" : "none", delay: level * 50, children: jsxs(OptimizedGlassCore, { elevation: "level1", intensity: "subtle", depth: 0.5, tint: "neutral", border: variant === "bordered" ? "subtle" : "none", className: cn("glass-tree-node flex items-center cursor-pointer transition-all duration-200 glass-radius-md", config.node, colors.node, isSelected && colors.selected, isDragOver && colors.dragOver, node.disabled && "opacity-50 cursor-not-allowed", variant === "bordered" && "border glass-radius-md"), style: { paddingLeft: `${level * indentation}px` }, onClick: handleNodeClick, onDragStart: handleDragStart, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, draggable: draggable && !node.disabled, id: nodeId, role: "treeitem", "aria-selected": isSelected, "aria-expanded": hasChildren ? isExpanded : undefined, "aria-level": level + 1, "aria-disabled": node.disabled, tabIndex: node.disabled ? -1 : 0, children: [showLines && level > 0 && jsxs("div", { className: 'absolute left-0 top-0 glass-h-full glass-flex glass-items-center', children: [jsx("div", { className: "glass-border-l glass-border-glass-border/20 glass-h-full", style: { left: `${(level - 1) * indentation + indentation / 2}px` } }), jsx("div", { className: 'glass-border-t glass-border-glass-border/20 w-2', style: { left: `${(level - 1) * indentation + indentation / 2}px` } })] }), jsx("div", { className: cn("flex items-center justify-center flex-shrink-0 glass-mr-1", config.expandIcon), children: hasChildren ? jsx("button", { onClick: handleToggle, className: 'glass-p-1 glass-radius-md hover:glass-surface-overlay transition-colors glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', "aria-label": isExpanded ? "Collapse" : "Expand", children: isExpanded ? collapseIcon || defaultCollapseIcon : expandIcon || defaultExpandIcon }) : jsx("div", { className: config.expandIcon }) // Spacer }), showCheckboxes && jsx("input", { type: "checkbox", checked: isChecked, onChange: handleCheck, disabled: node.disabled, className: cn("flex-shrink-0 glass-mr-2 glass-radius-md", config.checkbox), onClick: e => e.stopPropagation() }), showIcons && node.icon && jsx("div", { className: cn("flex-shrink-0 glass-mr-2", config.icon), children: node.icon }), jsx("span", { className: 'glass-flex-1 truncate', children: node.label }), isDragOver && jsx("div", { className: cn("absolute left-0 right-0 h-0.5 bg-primary pointer-events-none", isDragOver === "before" && "top-0", isDragOver === "after" && "bottom-0", isDragOver === "inside" && "top-1/2 transform -translate-y-1/2 h-full bg-primary/10") }), hasChildren && isExpanded && jsx(MotionFramer, { preset: shouldAnimate && respectMotionPreference ? "slideDown" : "none", className: 'relative', children: node.children?.map(child => jsx(GlassTreeNodeComponent, { node: child, level: level + 1 }, child.id)) })] }) }); }; const GlassTreeView = /*#__PURE__*/forwardRef(({ // TODO: Integrate ContrastGuard for table cells, list items, badges, card titles, and other text content for WCAG AA compliance data = DEFAULT_TREE_DATA, selectedId, expandedIds, defaultExpandedIds = [], selectionMode = "single", selectedIds, defaultSelectedIds = [], showLines = true, showIcons = true, draggable = false, showCheckboxes = false, size = "md", variant = "default", indentation = 24, renderNode, expandIcon, collapseIcon, onSelect, onSelectionChange, onExpand, onDragStart, onDrop, respectMotionPreference = true, className, ...props }, ref) => { const treeId = useA11yId("glass-tree"); const [internalExpandedIds, setInternalExpandedIds] = useState(expandedIds || defaultExpandedIds); const [internalSelectedIds, setInternalSelectedIds] = useState(selectedIds || defaultSelectedIds); const currentExpandedIds = expandedIds !== undefined ? expandedIds : internalExpandedIds; const currentSelectedIds = selectedIds !== undefined ? selectedIds : internalSelectedIds; const handleNodeSelect = useCallback((nodeId, node) => { if (selectionMode === "single") { onSelect?.(nodeId, node); } else if (selectionMode === "multiple") { let newSelection; if (currentSelectedIds.includes(nodeId)) { newSelection = currentSelectedIds.filter(id => id !== nodeId); } else { newSelection = [...currentSelectedIds, nodeId]; } if (selectedIds === undefined) { setInternalSelectedIds(newSelection); } // Find all nodes for the callback const findNodes = (nodes, ids) => { const result = []; for (const node of nodes) { if (ids.includes(node.id)) result.push(node); if (node.children) result.push(...findNodes(node.children, ids)); } return result; }; onSelectionChange?.(newSelection, findNodes(data, newSelection)); } }, [selectionMode, currentSelectedIds, selectedIds, onSelect, onSelectionChange, data]); const handleNodeToggle = useCallback(nodeId => { let newExpandedIds; if (currentExpandedIds.includes(nodeId)) { newExpandedIds = currentExpandedIds.filter(id => id !== nodeId); } else { newExpandedIds = [...currentExpandedIds, nodeId]; } if (expandedIds === undefined) { setInternalExpandedIds(newExpandedIds); } onExpand?.(nodeId, !currentExpandedIds.includes(nodeId)); }, [currentExpandedIds, expandedIds, onExpand]); const handleNodeCheck = useCallback((nodeId, checked) => { handleNodeSelect(nodeId, data.find(node => node.id === nodeId)); }, [handleNodeSelect, data]); const contextValue = { selectedId, selectedIds: currentSelectedIds, expandedIds: currentExpandedIds, selectionMode, showLines, showIcons, showCheckboxes: showCheckboxes || selectionMode === "multiple", draggable, size, variant, indentation, expandIcon, collapseIcon, renderNode, respectMotionPreference, onNodeSelect: handleNodeSelect, onNodeToggle: handleNodeToggle, onNodeCheck: handleNodeCheck, onDragStart, onDrop }; return jsx(TreeContext.Provider, { value: contextValue, children: jsx(OptimizedGlassCore, { ref: ref, id: treeId, elevation: "level1", intensity: "subtle", depth: 1, tint: "neutral", border: "subtle", className: cn("glass-tree-view glass-p-2 glass-radius-lg glass-backdrop-blur-md", "border border-border/20", className), role: "tree", "aria-multiselectable": selectionMode === "multiple", ...props, children: data.map(node => jsx(GlassTreeNodeComponent, { node: node, level: 0 }, node.id)) }) }); }); GlassTreeView.displayName = "GlassTreeView"; export { GlassTreeView, GlassTreeView as default }; //# sourceMappingURL=GlassTreeView.js.map