aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
696 lines (693 loc) • 25.6 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { forwardRef, useRef, useState, useCallback, useEffect } 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 { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js';
import { useGlassSound } from '../../utils/soundDesign.js';
const GlassPatternBuilder = /*#__PURE__*/forwardRef(({
width = 800,
height = 600,
layers = [],
activeLayerIndex = 0,
selectedElements = [],
showGrid = true,
gridSize = 20,
snapToGrid = false,
zoom = 1,
templates = [],
colorPalette = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#F8C471"],
showRulers = true,
backgroundColor = "var(--glass-white)",
exportFormat = "png",
onChange,
onLayerChange,
onElementSelect,
onTemplateApply,
onExport,
showControls = true,
showLayerPanel = true,
showProperties = true,
respectMotionPreference = true,
className,
...props
}, ref) => {
const {
prefersReducedMotion,
isMotionSafe
} = useMotionPreferenceContext();
const {
play
} = useGlassSound();
const canvasRef = useRef(null);
const patternBuilderId = useA11yId("glass-pattern-builder");
const [currentLayers, setCurrentLayers] = useState(layers.length > 0 ? layers : [{
name: "Layer 1",
elements: [],
visible: true,
locked: false,
opacity: 1,
blendMode: "normal",
id: "layer-1"
}]);
const [activeLayer, setActiveLayer] = useState(activeLayerIndex);
const [selectedElementIds, setSelectedElementIds] = useState(selectedElements);
const [currentTool, setCurrentTool] = useState("circle");
const [currentColor, setCurrentColor] = useState(colorPalette[0]);
const [isDrawing, setIsDrawing] = useState(false);
const [dragStart, setDragStart] = useState(null);
const [currentZoom, setCurrentZoom] = useState(zoom);
const [panOffset, setPanOffset] = useState({
x: 0,
y: 0
});
const [history, setHistory] = useState([currentLayers]);
const [historyIndex, setHistoryIndex] = useState(0);
// Default templates
const defaultTemplates = [{
name: "Geometric Grid",
category: "Abstract",
preview: "grid-preview",
id: "template-grid",
layers: [{
name: "Grid Layer",
id: "grid-layer",
visible: true,
locked: false,
opacity: 1,
blendMode: "normal",
elements: Array.from({
length: 25
}, (_, i) => ({
type: "square",
x: i % 5 * 100 + 50,
y: Math.floor(i / 5) * 100 + 50,
width: 60,
height: 60,
rotation: 0,
color: colorPalette[i % colorPalette.length],
opacity: 0.8,
strokeColor: "var(--glass-black)",
strokeWidth: 2,
id: `grid-${i}`,
properties: {}
}))
}]
}, {
name: "Concentric Circles",
category: "Organic",
preview: "circles-preview",
id: "template-circles",
layers: [{
name: "Circles Layer",
id: "circles-layer",
visible: true,
locked: false,
opacity: 1,
blendMode: "normal",
elements: Array.from({
length: 8
}, (_, i) => ({
type: "circle",
x: width / 2,
y: height / 2,
width: (i + 1) * 40,
height: (i + 1) * 40,
rotation: 0,
color: "transparent",
opacity: 0.6,
strokeColor: colorPalette[i % colorPalette.length],
strokeWidth: 3,
id: `circle-${i}`,
properties: {}
}))
}]
}, {
name: "Mandala",
category: "Decorative",
preview: "mandala-preview",
id: "template-mandala",
layers: [{
name: "Mandala Layer",
id: "mandala-layer",
visible: true,
locked: false,
opacity: 1,
blendMode: "normal",
elements: Array.from({
length: 12
}, (_, i) => ({
type: "circle",
x: width / 2 + Math.cos(i * Math.PI / 6) * 100,
y: height / 2 + Math.sin(i * Math.PI / 6) * 100,
width: 40,
height: 40,
rotation: 0,
color: colorPalette[i % 3],
opacity: 0.7,
strokeColor: "var(--glass-black)",
strokeWidth: 1,
id: `mandala-${i}`,
properties: {}
}))
}]
}];
const allTemplates = [...defaultTemplates, ...templates];
// Get mouse position relative to canvas
const getCanvasPos = useCallback(event => {
const canvas = canvasRef.current;
if (!canvas) return {
x: 0,
y: 0
};
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left - panOffset.x) / currentZoom;
const y = (event.clientY - rect.top - panOffset.y) / currentZoom;
if (snapToGrid) {
return {
x: Math.round(x / gridSize) * gridSize,
y: Math.round(y / gridSize) * gridSize
};
}
return {
x,
y
};
}, [panOffset, currentZoom, snapToGrid, gridSize]);
// Create new element
const createElement = useCallback((x, y) => {
return {
type: currentTool,
x,
y,
width: 50,
height: 50,
rotation: 0,
color: currentColor,
opacity: 1,
strokeColor: "var(--glass-black)",
strokeWidth: 2,
id: `element-${Date.now()}-${Math.random()}`,
properties: {}
};
}, [currentTool, currentColor]);
// Handle canvas mouse events
const handleMouseDown = useCallback(event => {
const pos = getCanvasPos(event);
// Check if clicking on existing element
const clickedElement = currentLayers[activeLayer]?.elements.find(element => {
return pos.x >= element.x - element.width / 2 && pos.x <= element.x + element.width / 2 && pos.y >= element.y - element.height / 2 && pos.y <= element.y + element.height / 2;
});
if (clickedElement) {
// Select element
setSelectedElementIds([clickedElement.id]);
onElementSelect?.([clickedElement.id]);
} else {
// Start drawing new element
setIsDrawing(true);
setDragStart(pos);
setSelectedElementIds([]);
onElementSelect?.([]);
}
play("tap");
}, [getCanvasPos, currentLayers, activeLayer, onElementSelect, play]);
const handleMouseMove = useCallback(event => {
if (!isDrawing || !dragStart) return;
// Update drawing preview or element size
// This would be implemented for interactive drawing
}, [isDrawing, dragStart]);
const handleMouseUp = useCallback(event => {
if (!isDrawing || !dragStart) return;
getCanvasPos(event);
const newElement = createElement(dragStart.x, dragStart.y);
// Add element to active layer
const updatedLayers = [...currentLayers];
if (updatedLayers[activeLayer]) {
updatedLayers[activeLayer] = {
...updatedLayers[activeLayer],
elements: [...updatedLayers[activeLayer].elements, newElement]
};
setCurrentLayers(updatedLayers);
addToHistory(updatedLayers);
onChange?.(updatedLayers);
onLayerChange?.(updatedLayers, activeLayer);
}
setIsDrawing(false);
setDragStart(null);
play("success");
}, [isDrawing, dragStart, getCanvasPos, createElement, currentLayers, activeLayer, onChange, onLayerChange, play]);
// Add to history for undo/redo
const addToHistory = useCallback(newLayers => {
const newHistory = history.slice(0, historyIndex + 1);
newHistory.push([...newLayers]);
if (newHistory.length > 50) {
newHistory.shift();
} else {
setHistoryIndex(historyIndex + 1);
}
setHistory(newHistory);
}, [history, historyIndex]);
// Undo/Redo
const undo = useCallback(() => {
if (historyIndex > 0) {
setHistoryIndex(historyIndex - 1);
setCurrentLayers(history[historyIndex - 1]);
play("tap");
}
}, [historyIndex, history, play]);
const redo = useCallback(() => {
if (historyIndex < history.length - 1) {
setHistoryIndex(historyIndex + 1);
setCurrentLayers(history[historyIndex + 1]);
play("tap");
}
}, [historyIndex, history, play]);
// Layer operations
const addLayer = useCallback(() => {
const newLayer = {
name: `Layer ${currentLayers.length + 1}`,
elements: [],
visible: true,
locked: false,
opacity: 1,
blendMode: "normal",
id: `layer-${Date.now()}`
};
const updatedLayers = [...currentLayers, newLayer];
setCurrentLayers(updatedLayers);
setActiveLayer(updatedLayers.length - 1);
addToHistory(updatedLayers);
onLayerChange?.(updatedLayers, updatedLayers.length - 1);
play("success");
}, [currentLayers, addToHistory, onLayerChange, play]);
const deleteLayer = useCallback(layerIndex => {
if (currentLayers.length <= 1) return;
const updatedLayers = currentLayers.filter((_, index) => index !== layerIndex);
setCurrentLayers(updatedLayers);
setActiveLayer(Math.min(activeLayer, updatedLayers.length - 1));
addToHistory(updatedLayers);
onLayerChange?.(updatedLayers, Math.min(activeLayer, updatedLayers.length - 1));
play("error");
}, [currentLayers, activeLayer, addToHistory, onLayerChange, play]);
// Apply template
const applyTemplate = useCallback(template => {
setCurrentLayers(template.layers);
setActiveLayer(0);
addToHistory(template.layers);
onTemplateApply?.(template);
onChange?.(template.layers);
play("success");
}, [addToHistory, onTemplateApply, onChange, play]);
// Export pattern
const exportPattern = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
switch (exportFormat) {
case "png":
const pngData = canvas.toDataURL("image/png");
onExport?.(pngData, "png");
break;
case "svg":
// Generate SVG data
const svgData = generateSVG(currentLayers, width, height);
onExport?.(svgData, "svg");
break;
case "json":
const jsonData = JSON.stringify({
layers: currentLayers,
width,
height
}, null, 2);
onExport?.(jsonData, "json");
break;
}
play("success");
}, [currentLayers, width, height, exportFormat, onExport, play]);
// Generate SVG
const generateSVG = useCallback((layers, w, h) => {
let svg = `<svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`;
svg += `<rect width="100%" height="100%" fill="${backgroundColor}"/>`;
layers.forEach(layer => {
if (!layer.visible) return;
svg += `<g opacity="${layer.opacity}">`;
layer.elements.forEach(element => {
switch (element.type) {
case "circle":
svg += `<circle cx="${element.x}" cy="${element.y}" r="${element.width / 2}"
fill="${element.color}" stroke="${element.strokeColor}" stroke-width="${element.strokeWidth}"
opacity="${element.opacity}" transform="rotate(${element.rotation} ${element.x} ${element.y})"/>`;
break;
case "square":
svg += `<rect x="${element.x - element.width / 2}" y="${element.y - element.height / 2}"
width="${element.width}" height="${element.height}"
fill="${element.color}" stroke="${element.strokeColor}" stroke-width="${element.strokeWidth}"
opacity="${element.opacity}" transform="rotate(${element.rotation} ${element.x} ${element.y})"/>`;
break;
// Add more shapes as needed
}
});
svg += "</g>";
});
svg += "</svg>";
return svg;
}, [backgroundColor]);
// Render canvas
const render = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Clear canvas
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, width, height);
// Apply zoom and pan
ctx.save();
ctx.translate(panOffset.x, panOffset.y);
ctx.scale(currentZoom, currentZoom);
// Draw grid
if (showGrid) {
ctx.strokeStyle = "rgba(var(--glass-color-black) / var(--glass-opacity-10))";
ctx.lineWidth = 1 / currentZoom;
for (let x = 0; x <= width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
for (let y = 0; y <= height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
}
// Draw layers
currentLayers.forEach((layer, layerIndex) => {
if (!layer.visible) return;
ctx.save();
ctx.globalAlpha = layer.opacity;
layer.elements.forEach(element => {
ctx.save();
ctx.translate(element.x, element.y);
ctx.rotate(element.rotation * Math.PI / 180);
ctx.globalAlpha = element.opacity;
// Draw element based on type
switch (element.type) {
case "circle":
ctx.fillStyle = element.color;
ctx.strokeStyle = element.strokeColor;
ctx.lineWidth = element.strokeWidth;
ctx.beginPath();
ctx.arc(0, 0, element.width / 2, 0, Math.PI * 2);
if (element.color !== "transparent") ctx.fill();
if (element.strokeWidth > 0) ctx.stroke();
break;
case "square":
ctx.fillStyle = element.color;
ctx.strokeStyle = element.strokeColor;
ctx.lineWidth = element.strokeWidth;
if (element.color !== "transparent") {
ctx.fillRect(-element.width / 2, -element.height / 2, element.width, element.height);
}
if (element.strokeWidth > 0) {
ctx.strokeRect(-element.width / 2, -element.height / 2, element.width, element.height);
}
break;
case "triangle":
ctx.fillStyle = element.color;
ctx.strokeStyle = element.strokeColor;
ctx.lineWidth = element.strokeWidth;
ctx.beginPath();
ctx.moveTo(0, -element.height / 2);
ctx.lineTo(-element.width / 2, element.height / 2);
ctx.lineTo(element.width / 2, element.height / 2);
ctx.closePath();
if (element.color !== "transparent") ctx.fill();
if (element.strokeWidth > 0) ctx.stroke();
break;
case "line":
ctx.strokeStyle = element.strokeColor || element.color;
ctx.lineWidth = element.strokeWidth;
ctx.beginPath();
ctx.moveTo(-element.width / 2, 0);
ctx.lineTo(element.width / 2, 0);
ctx.stroke();
break;
}
// Highlight selected elements
if (selectedElementIds.includes(element.id)) {
ctx.strokeStyle = "var(--glass-color-primary)";
ctx.lineWidth = 2 / currentZoom;
ctx.setLineDash([5 / currentZoom, 5 / currentZoom]);
ctx.strokeRect(-element.width / 2 - 5, -element.height / 2 - 5, element.width + 10, element.height + 10);
ctx.setLineDash([]);
}
ctx.restore();
});
ctx.restore();
});
ctx.restore();
// Draw rulers
if (showRulers) {
ctx.fillStyle = "rgba(var(--glass-color-black) / var(--glass-opacity-10))";
ctx.fillRect(0, 0, width, 20);
ctx.fillRect(0, 0, 20, height);
ctx.fillStyle = "#333";
ctx.font = "10px monospace";
ctx.textAlign = "center";
// Horizontal ruler
for (let x = 0; x < width; x += 50) {
ctx.fillText(x.toString(), x + panOffset.x, 15);
}
// Vertical ruler
ctx.save();
ctx.rotate(-Math.PI / 2);
for (let y = 0; y < height; y += 50) {
ctx.fillText(y.toString(), -y - panOffset.y, 15);
}
ctx.restore();
}
}, [backgroundColor, width, height, panOffset, currentZoom, showGrid, gridSize, currentLayers, selectedElementIds, showRulers]);
// Animation loop
useEffect(() => {
const animate = () => {
render();
if (!prefersReducedMotion) {
requestAnimationFrame(animate);
}
};
animate();
}, [render, prefersReducedMotion]);
// Canvas setup
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
canvas.width = width;
canvas.height = height;
}, [width, height]);
// Tool panel
const renderToolPanel = () => {
if (!showControls) return null;
return jsxs(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: "glass-pattern-tools glass-flex glass-flex-wrap glass-items-center glass-gap-4 glass-p-4 glass-radius-lg glass-glass-glass-backdrop-blur-md glass-contrast-guard glass-border glass-border-glass-border/20 glass-contrast-guard",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("span", {
className: 'glass-text-sm font-medium',
children: "Tool:"
}), ["circle", "square", "triangle", "line"].map(tool => jsx("button", {
onClick: () => setCurrentTool(tool),
className: cn("glass-px-3 glass-py-1 glass-radius-md transition-colors capitalize glass-focus glass-touch-target glass-contrast-guard", currentTool === tool ? "bg-primary/20 text-primary" : "bg-background/20 hover:bg-background/30"),
children: tool
}, tool))]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("span", {
className: 'glass-text-sm font-medium',
children: "Color:"
}), jsx("div", {
className: "glass-flex glass-gap-1",
children: colorPalette.map((color, i) => jsx("button", {
onClick: () => setCurrentColor(color),
className: cn("w-6 h-6 glass-radius-sm border-2 transition-all glass-focus glass-touch-target glass-contrast-guard", currentColor === color ? "border-primary" : "border-border/30"),
style: {
backgroundColor: color
}
}, `${color}-${i}`))
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("button", {
onClick: undo,
className: 'glass-focus glass-touch-target glass-contrast-guard glass-radius-md glass-surface-overlay hover:glass-surface-overlay glass-px-3 glass-py-1',
children: "Undo"
}), jsx("button", {
onClick: redo,
className: 'glass-focus glass-touch-target glass-contrast-guard glass-radius-md glass-surface-overlay hover:glass-surface-overlay glass-px-3 glass-py-1',
children: "Redo"
}), jsx("button", {
onClick: exportPattern,
className: 'glass-focus glass-touch-target glass-contrast-guard glass-radius-md glass-surface-primary/20 hover:glass-surface-primary/30 glass-px-3 glass-py-1 text-primary',
children: "Export"
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("span", {
className: "glass-text-sm",
children: "Zoom:"
}), jsx("input", {
type: "range",
min: "0.5",
max: "3",
step: "0.1",
value: currentZoom,
onChange: e => setCurrentZoom(parseFloat(e.target.value)),
className: 'w-20 glass-focus glass-touch-target glass-contrast-guard'
}), jsxs("span", {
className: 'glass-text-sm min-w-[3ch]',
children: [Math.round(currentZoom * 100), "%"]
})]
})]
});
};
// Layer panel
const renderLayerPanel = () => {
if (!showLayerPanel) return null;
return jsxs(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: "glass-layer-panel glass-p-4 glass-radius-lg glass-glass-glass-backdrop-blur-md glass-contrast-guard glass-border glass-border-glass-border/20 glass-contrast-guard",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-3',
children: [jsx("span", {
className: 'glass-text-sm font-medium',
children: "Layers"
}), jsx("button", {
onClick: addLayer,
className: 'glass-px-2 glass-py-1 glass-radius-md glass-surface-primary/20 hover:glass-surface-primary/30 text-primary glass-text-xs glass-focus glass-touch-target glass-contrast-guard',
children: "Add"
})]
}), jsx("div", {
className: 'space-y-2',
children: currentLayers.map((layer, index) => jsxs("div", {
className: cn("glass-p-2 glass-radius-md border transition-colors cursor-pointer glass-focus glass-touch-target glass-contrast-guard", index === activeLayer ? "border-primary/50 bg-primary/10" : "border-border/20 bg-background/10 hover:bg-background/20"),
onClick: () => setActiveLayer(index),
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsx("span", {
className: 'glass-text-sm font-medium',
children: layer.name
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-1",
children: [jsx("button", {
onClick: e => {
e.stopPropagation();
const updated = [...currentLayers];
updated[index] = {
...updated[index],
visible: !updated[index].visible
};
setCurrentLayers(updated);
},
className: 'glass-text-xs glass-px-1 hover:glass-surface-overlay glass-radius-sm glass-focus glass-touch-target glass-contrast-guard',
children: layer.visible ? "👁" : "👁🗨"
}), jsx("button", {
onClick: e => {
e.stopPropagation();
deleteLayer(index);
},
className: 'glass-text-xs glass-px-1 hover:glass-surface-red/20 glass-radius-sm text-primary glass-focus glass-touch-target glass-contrast-guard',
children: "\uD83D\uDDD1"
})]
})]
}), jsxs("div", {
className: "glass-text-xs glass-text-secondary glass-mt-1",
children: [layer.elements.length, " elements"]
})]
}, layer.id))
})]
});
};
// Template panel
const renderTemplates = () => {
return jsxs(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: "glass-templates glass-p-4 glass-radius-lg glass-glass-glass-backdrop-blur-md glass-contrast-guard glass-border glass-border-glass-border/20 glass-contrast-guard",
children: [jsx("div", {
className: 'glass-text-sm font-medium mb-3',
children: "Templates"
}), jsx("div", {
className: 'space-y-2',
children: allTemplates.map(template => jsxs("button", {
onClick: () => applyTemplate(template),
className: 'glass-w-full glass-p-2 glass-radius-md glass-surface-overlay hover:glass-surface-overlay text-left glass-focus glass-touch-target glass-contrast-guard',
children: [jsx("div", {
className: 'glass-text-sm font-medium',
children: template.name
}), jsx("div", {
className: "glass-text-xs glass-text-secondary",
children: template.category
})]
}, template.id))
})]
});
};
return jsx(OptimizedGlassCore, {
ref: ref,
id: patternBuilderId,
elevation: "level1",
intensity: "subtle",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-pattern-builder relative glass-radius-lg glass-glass-backdrop-blur-md glass-contrast-guard border border-border/20", className),
...props,
children: jsxs(MotionFramer, {
preset: isMotionSafe && respectMotionPreference ? "fadeIn" : "none",
className: "glass-flex glass-flex-col glass-gap-4 glass-p-4",
children: [renderToolPanel(), jsxs("div", {
className: "glass-flex glass-gap-4",
children: [showLayerPanel && jsxs("div", {
className: 'w-64 space-y-4',
children: [renderLayerPanel(), renderTemplates()]
}), jsx("div", {
className: "glass-flex-1",
children: jsx("canvas", {
ref: canvasRef,
width: width,
height: height,
className: 'glass-border glass-border-glass-border/20 glass-radius-md glass-surface-subtle cursor-crosshair',
onMouseDown: handleMouseDown,
onMouseMove: handleMouseMove,
onMouseUp: handleMouseUp,
style: {
width,
height
}
})
})]
})]
})
});
});
GlassPatternBuilder.displayName = "GlassPatternBuilder";
export { GlassPatternBuilder, GlassPatternBuilder as default };
//# sourceMappingURL=GlassPatternBuilder.js.map