UNPKG

aura-glass

Version:

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

362 lines (359 loc) 16.1 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useMemo, useState, useEffect, useCallback } 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 '../../primitives/motion/MotionFramer.js'; const EMPTY_EXAMPLES = []; const EMPTY_TABS = []; const shallowEqual = (a, b) => { if (a === b) return true; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (a[key] !== b[key]) { return false; } } return true; }; const GlassComponentPlayground = ({ examples: incomingExamples = EMPTY_EXAMPLES, defaultExample, showCode = true, showProps = true, customTabs: incomingCustomTabs = EMPTY_TABS, className = "", theme = "dark", codeTheme = "dark" }) => { const examples = useMemo(() => Array.isArray(incomingExamples) ? incomingExamples : EMPTY_EXAMPLES, [incomingExamples]); const tabs = useMemo(() => Array.isArray(incomingCustomTabs) ? incomingCustomTabs : EMPTY_TABS, [incomingCustomTabs]); const defaultExampleId = useMemo(() => { if (defaultExample && examples.some(ex => ex.id === defaultExample)) { return defaultExample; } return examples[0]?.id || ""; }, [defaultExample, examples]); const [selectedExample, setSelectedExample] = useState(defaultExampleId); const [activeTab, setActiveTab] = useState("preview"); const [componentProps, setComponentProps] = useState(() => { const initialExample = examples.find(ex => ex.id === defaultExampleId); return { ...(initialExample?.props ?? {}) }; }); const [selectedCategory, setSelectedCategory] = useState("all"); useEffect(() => { if (!examples.some(ex => ex.id === selectedExample)) { if (defaultExampleId !== selectedExample) { setSelectedExample(defaultExampleId); } } }, [examples, selectedExample, defaultExampleId]); useEffect(() => { if (defaultExampleId && defaultExampleId !== selectedExample) { setSelectedExample(defaultExampleId); } }, [defaultExampleId, selectedExample]); useEffect(() => { const example = examples.find(ex => ex.id === selectedExample); setComponentProps(prev => { if (!example) { return Object.keys(prev).length === 0 ? prev : {}; } const nextProps = example.props ?? {}; return shallowEqual(prev, nextProps) ? prev : { ...nextProps }; }); }, [examples, selectedExample]); const categories = useMemo(() => { return Array.from(new Set(examples.map(ex => ex?.category).filter(Boolean))); }, [examples]); // Generate code from component and props const generateCodeFromProps = useCallback((component, props) => { const componentName = component.displayName || component.name || "Component"; const propsString = Object.entries(props).map(([key, value]) => { if (typeof value === "string") { return `${key}="${value}"`; } else if (typeof value === "boolean") { return value ? key : `${key}={false}`; } else if (typeof value === "number") { return `${key}={${value}}`; } else if (Array.isArray(value)) { return `${key}={${JSON.stringify(value)}}`; } else if (typeof value === "object") { return `${key}={${JSON.stringify(value, null, 2)}}`; } return `${key}={${String(value)}}`; }).join("\n "); return `<${componentName}${propsString ? `\n ${propsString}` : ""}\n/>`; }, []); // Handle prop change const handlePropChange = useCallback((propName, value) => { setComponentProps(prev => ({ ...prev, [propName]: value })); }, []); // Filter examples by category const filteredExamples = useMemo(() => { if (selectedCategory === "all") { return examples; } return examples.filter(ex => ex?.category === selectedCategory); }, [examples, selectedCategory]); const currentExample = useMemo(() => examples.find(ex => ex.id === selectedExample) || null, [examples, selectedExample]); const componentCode = useMemo(() => { if (!currentExample) { return ""; } if (currentExample.code) { return currentExample.code; } return generateCodeFromProps(currentExample.component, componentProps); }, [currentExample, componentProps, generateCodeFromProps]); const handleCopyCode = useCallback(() => { if (typeof navigator === "undefined") { return; } try { navigator.clipboard?.writeText(componentCode).catch(() => undefined); } catch { // Ignore clipboard write failures outside the browser } }, [componentCode]); // Render prop editor const renderPropEditor = () => { if (!showProps || !currentExample) return null; const propKeys = Object.keys(componentProps); return jsxs("div", { "data-glass-component": true, className: "glass-gap-4", children: [jsx("h4", { className: 'glass-text-sm font-semibold text-primary mb-3', children: "Component Props" }), propKeys.length === 0 ? jsx("p", { className: 'glass-text-sm text-primary/50', children: "No editable props available" }) : propKeys.map(propName => { const value = componentProps[propName]; const valueType = typeof value; return jsxs("div", { className: "glass-gap-2", children: [jsxs("label", { className: 'block glass-text-sm text-primary/70', children: [propName, " (", valueType, ")"] }), valueType === "string" && jsx("input", { type: "text", value: value, onChange: e => handlePropChange(propName, e.target.value), className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-md text-primary placeholder-white/50 focus:outline-none focus:border-white/40 glass-focus glass-touch-target glass-contrast-guard' }), valueType === "number" && jsx("input", { type: "number", value: value, onChange: e => handlePropChange(propName, Number(e.target.value)), className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-md text-primary placeholder-white/50 focus:outline-none focus:border-white/40 glass-focus glass-touch-target glass-contrast-guard' }), valueType === "boolean" && jsxs("label", { className: 'glass-flex glass-items-center glass-gap-2 cursor-pointer', children: [jsx("input", { type: "checkbox", checked: value, onChange: e => handlePropChange(propName, e.target.checked), className: 'glass-radius-md glass-border-white/20 text-primary focus:ring-blue-500 glass-focus glass-touch-target glass-contrast-guard' }), jsx("span", { className: 'glass-text-sm text-primary/70', children: value ? "True" : "False" })] }), (valueType === "object" || Array.isArray(value)) && jsx("textarea", { value: JSON.stringify(value, null, 2), onChange: e => { try { const parsed = JSON.parse(e.target.value); handlePropChange(propName, parsed); } catch (err) { // Invalid JSON, don't update } }, rows: 4, className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-md text-primary placeholder-white/50 focus:outline-none focus:border-white/40 font-mono glass-text-sm' })] }, propName); })] }); }; return jsxs(OptimizedGlassCore, { className: `flex flex-col h-full ${className}`, intent: "neutral", elevation: "level1", children: [jsxs("div", { className: "glass-flex glass-items-center glass-justify-between glass-p-4 glass-border-b glass-border-white/10", children: [jsx("h2", { className: 'glass-text-xl font-semibold text-primary', children: "Component Playground" }), jsx("div", { className: "glass-flex glass-items-center glass-gap-4", children: jsxs("select", { value: selectedCategory, onChange: e => setSelectedCategory(e.target.value), "aria-label": "Filter components by category", className: 'glass-px-3 glass-py-2 glass-surface-subtle/10 glass-border glass-border-white/20 glass-radius-md text-primary focus:outline-none focus:border-white/40 glass-focus glass-touch-target glass-contrast-guard', children: [jsx("option", { value: "all", children: "All Categories" }), categories.map(category => jsx("option", { value: category, children: category.charAt(0).toUpperCase() + category.slice(1) }, category))] }) })] }), jsxs("div", { className: 'glass-flex glass-flex-1 overflow-hidden', children: [jsxs("div", { className: 'w-64 glass-border-r glass-border-white/10 glass-p-4 overflow-y-auto', children: [jsx("h3", { className: 'glass-text-sm font-semibold text-primary mb-4', children: "Components" }), jsx("div", { className: "glass-gap-2", children: filteredExamples.map(example => jsxs("button", { onClick: e => setSelectedExample(example.id), className: `w-full text-left glass-p-3 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard ${selectedExample === example.id ? "bg-white/20 glass-text-primary" : "glass-text-primary/70 hover:glass-text-primary hover:bg-white/10"}`, children: [jsx("div", { className: 'font-medium glass-text-sm', children: example.name }), example.description && jsx("div", { className: 'glass-text-xs opacity-70 glass-mt-1', children: example.description }), jsx("div", { className: 'glass-text-xs opacity-50 glass-mt-1 capitalize', children: example.category })] }, example.id)) })] }), jsxs("div", { className: "glass-flex-1 glass-flex glass-flex-col", children: [jsxs("div", { className: "glass-flex glass-border-b glass-border-white/10", children: [jsx("button", { onClick: e => setActiveTab("preview"), className: `glass-px-4 glass-py-3 glass-text-sm font-medium transition-colors glass-focus glass-touch-target glass-contrast-guard ${activeTab === "preview" ? "glass-text-primary border-b-2 border-blue-400" : "glass-text-primary/70 hover:glass-text-primary"}`, children: "Preview" }), showCode && jsx("button", { onClick: e => setActiveTab("code"), className: `glass-px-4 glass-py-3 glass-text-sm font-medium transition-colors glass-focus glass-touch-target glass-contrast-guard ${activeTab === "code" ? "glass-text-primary border-b-2 border-blue-400" : "glass-text-primary/70 hover:glass-text-primary"}`, children: "Code" }), showProps && jsx("button", { onClick: e => setActiveTab("props"), className: `glass-px-4 glass-py-3 glass-text-sm font-medium transition-colors glass-focus glass-touch-target glass-contrast-guard ${activeTab === "props" ? "glass-text-primary border-b-2 border-blue-400" : "glass-text-primary/70 hover:glass-text-primary"}`, children: "Props" }), tabs.map(tab => jsx("button", { onClick: e => setActiveTab(tab.id), className: `glass-px-4 glass-py-3 glass-text-sm font-medium transition-colors glass-focus glass-touch-target glass-contrast-guard ${activeTab === tab.id ? "glass-text-primary border-b-2 border-blue-400" : "glass-text-primary/70 hover:glass-text-primary"}`, children: tab.label }, tab.id))] }), jsxs("div", { className: 'glass-flex-1 overflow-hidden', children: [activeTab === "preview" && jsx("div", { className: 'glass-h-full glass-p-6 overflow-auto', children: jsx("div", { className: 'max-w-4xl glass-mx-auto', children: currentExample ? jsxs("div", { className: 'space-y-6', children: [jsxs("div", { children: [jsx("h3", { className: 'glass-text-lg font-semibold text-primary mb-2', children: currentExample.name }), currentExample.description && jsx("p", { className: 'text-primary/70', children: currentExample.description })] }), jsx(OptimizedGlassCore, { className: "glass-p-6 glass-min-h-64 glass-flex glass-items-center glass-justify-center", blur: "subtle", elevation: "level1", children: jsx(currentExample.component, { ...componentProps }) })] }) : jsx("div", { className: 'glass-flex glass-items-center glass-justify-center h-64 text-primary/50', children: "Select a component to preview" }) }) }), activeTab === "code" && showCode && jsx("div", { className: 'glass-h-full glass-p-6 overflow-auto', children: jsxs("div", { className: "glass-gap-4", children: [jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsx("h3", { className: 'glass-text-lg font-semibold text-primary', children: "Generated Code" }), jsx("button", { onClick: handleCopyCode, className: 'glass-px-3 glass-py-1 glass-text-sm glass-surface-subtle/10 hover:glass-surface-subtle/20 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard', children: "Copy" })] }), jsx("pre", { className: 'glass-p-4 glass-surface-dark/20 glass-radius-md overflow-x-auto', children: jsx("code", { className: 'glass-text-sm text-primary font-mono whitespace-pre-wrap', children: componentCode }) })] }) }), activeTab === "props" && showProps && jsx("div", { className: 'glass-h-full glass-p-6 overflow-y-auto', children: renderPropEditor() }), tabs.map(tab => activeTab === tab.id ? jsx("div", { className: 'glass-h-full glass-p-6 overflow-auto', children: tab.content }, tab.id) : null)] })] })] })] }); }; // Utility hook for creating playground examples const usePlaygroundExample = (component, defaultProps = {}) => { const [props, setProps] = useState(defaultProps); const updateProp = useCallback((propName, value) => { setProps(prev => ({ ...prev, [propName]: value })); }, []); const resetProps = useCallback(() => { setProps(defaultProps); }, [defaultProps]); return { props, updateProp, resetProps, component }; }; // Example usage helper const createPlaygroundExample = (id, name, component, options = {}) => { const { category = "general", description, defaultProps = {} } = options; return { id, name, description, category, component, props: defaultProps }; }; export { GlassComponentPlayground, createPlaygroundExample, usePlaygroundExample }; //# sourceMappingURL=GlassComponentPlayground.js.map