aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
362 lines (359 loc) • 16.1 kB
JavaScript
'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