aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
316 lines (313 loc) • 11.8 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { forwardRef, useRef, 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 { 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 GlassSignaturePad = /*#__PURE__*/forwardRef(({
width = 400,
height = 200,
options = {},
disabled = false,
showClearButton = true,
showSaveButton = true,
value,
onChange,
onBegin,
onEnd,
onClear,
onSave,
placeholder = "Sign here",
showPlaceholder = true,
exportFormat = "png",
exportQuality = 0.9,
respectMotionPreference = true,
className,
...props
}, ref) => {
const {
shouldAnimate
} = useMotionPreference();
const {
play
} = useGlassSound();
const canvasRef = useRef(null);
const contextRef = useRef(null);
const signaturePadId = useA11yId("glass-signature-pad");
const [isDrawing, setIsDrawing] = useState(false);
const [isEmpty, setIsEmpty] = useState(true);
const [points, setPoints] = useState([]);
const [lastVelocity, setLastVelocity] = useState(0);
const [lastWidth, setLastWidth] = useState(0);
const defaultOptions = {
minWidth: 0.5,
maxWidth: 2.5,
penColor: "var(--glass-black)",
backgroundColor: "transparent",
velocityFilterWeight: 0.7,
throttle: 16,
...options
};
// Initialize canvas
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext("2d");
if (!context) return;
contextRef.current = context;
// Set canvas size
canvas.width = width;
canvas.height = height;
// Configure context
context.lineCap = "round";
context.lineJoin = "round";
context.imageSmoothingEnabled = true;
// Set background
if (defaultOptions.backgroundColor !== "transparent") {
context.fillStyle = defaultOptions.backgroundColor;
context.fillRect(0, 0, width, height);
}
// Load existing signature if provided
if (value) {
const img = new Image();
img.onload = () => {
context.clearRect(0, 0, width, height);
if (defaultOptions.backgroundColor !== "transparent") {
context.fillStyle = defaultOptions.backgroundColor;
context.fillRect(0, 0, width, height);
}
context.drawImage(img, 0, 0);
setIsEmpty(false);
};
img.src = value;
}
}, [width, height, defaultOptions.backgroundColor, value]);
// Calculate stroke width based on velocity
const calculateStrokeWidth = useCallback(velocity => {
return Math.max(defaultOptions.maxWidth / (velocity + 1), defaultOptions.minWidth);
}, [defaultOptions.minWidth, defaultOptions.maxWidth]);
// Add point to signature
const addPoint = useCallback(point => {
const context = contextRef.current;
if (!context || points.length === 0) return;
const lastPoint = points[points.length - 1];
const distance = Math.sqrt(Math.pow(point.x - lastPoint.x, 2) + Math.pow(point.y - lastPoint.y, 2));
const time = point.timestamp - lastPoint.timestamp;
const velocity = time > 0 ? distance / time : 0;
// Apply velocity filter
const filteredVelocity = defaultOptions.velocityFilterWeight * velocity + (1 - defaultOptions.velocityFilterWeight) * lastVelocity;
const strokeWidth = calculateStrokeWidth(filteredVelocity);
// Smooth width transition
const smoothWidth = lastWidth * 0.3 + strokeWidth * 0.7;
context.beginPath();
context.moveTo(lastPoint.x, lastPoint.y);
context.lineTo(point.x, point.y);
context.strokeStyle = defaultOptions.penColor;
context.lineWidth = smoothWidth;
context.stroke();
setLastVelocity(filteredVelocity);
setLastWidth(smoothWidth);
setPoints(prev => [...prev, point]);
setIsEmpty(false);
}, [points, lastVelocity, lastWidth, defaultOptions.penColor, defaultOptions.velocityFilterWeight, calculateStrokeWidth]);
// Get pointer position
const getPointerPos = useCallback(event => {
const canvas = canvasRef.current;
if (!canvas) return {
x: 0,
y: 0
};
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}, []);
// Start signature
const handlePointerDown = useCallback(event => {
if (disabled) return;
event.preventDefault();
setIsDrawing(true);
const pos = getPointerPos(event);
const point = {
x: pos.x,
y: pos.y,
pressure: event.pressure || 1,
timestamp: Date.now()
};
setPoints([point]);
setLastVelocity(0);
setLastWidth(defaultOptions.minWidth);
onBegin?.();
play("signature_start");
}, [disabled, getPointerPos, defaultOptions.minWidth, onBegin, play]);
// Continue signature
const handlePointerMove = useCallback(event => {
if (!isDrawing || disabled) return;
const pos = getPointerPos(event);
const point = {
x: pos.x,
y: pos.y,
pressure: event.pressure || 1,
timestamp: Date.now()
};
addPoint(point);
}, [isDrawing, disabled, getPointerPos, addPoint]);
// End signature
const handlePointerUp = useCallback(() => {
if (!isDrawing) return;
setIsDrawing(false);
// Export signature
const canvas = canvasRef.current;
if (canvas) {
const dataUrl = canvas.toDataURL(`image/${exportFormat}`, exportQuality);
onChange?.(dataUrl);
}
onEnd?.();
play("signature_complete");
}, [isDrawing, exportFormat, exportQuality, onChange, onEnd, play]);
// Clear signature
const clear = useCallback(() => {
const canvas = canvasRef.current;
const context = contextRef.current;
if (!canvas || !context) return;
context.clearRect(0, 0, width, height);
if (defaultOptions.backgroundColor !== "transparent") {
context.fillStyle = defaultOptions.backgroundColor;
context.fillRect(0, 0, width, height);
}
setPoints([]);
setIsEmpty(true);
setIsDrawing(false);
onChange?.("");
onClear?.();
play("clear");
}, [width, height, defaultOptions.backgroundColor, onChange, onClear, play]);
// Save signature
const save = useCallback(() => {
const canvas = canvasRef.current;
if (!canvas || isEmpty) return;
const dataUrl = canvas.toDataURL(`image/${exportFormat}`, exportQuality);
onSave?.(dataUrl);
play("save");
}, [isEmpty, exportFormat, exportQuality, onSave, play]);
// Draw placeholder
const drawPlaceholder = useCallback(() => {
const canvas = canvasRef.current;
const context = contextRef.current;
if (!canvas || !context || !isEmpty || !showPlaceholder) return;
context.save();
context.fillStyle = "rgba(156, 163, 175, 0.5)";
context.font = "16px sans-serif";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText(placeholder, width / 2, height / 2);
context.restore();
}, [isEmpty, showPlaceholder, placeholder, width, height]);
// Redraw placeholder when needed
useEffect(() => {
if (isEmpty) {
const timeout = setTimeout(drawPlaceholder, 100);
return () => clearTimeout(timeout);
}
}, [isEmpty, drawPlaceholder]);
return jsx(OptimizedGlassCore, {
"data-glass-component": true,
ref: ref,
id: signaturePadId,
elevation: "level1",
intensity: "subtle",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-signature-pad glass-radius-lg glass-glass-backdrop-blur-md glass-contrast-guard border border-border/20", disabled && "opacity-50 pointer-events-none", className),
...props,
children: jsxs(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "fadeIn" : "none",
className: "glass-p-4 glass-gap-4",
children: [jsxs("div", {
className: 'relative',
children: [jsx("canvas", {
ref: canvasRef,
width: width,
height: height,
className: cn("border border-border/20 glass-radius-md bg-white cursor-crosshair", "touch-none",
// Prevent touch scrolling
disabled && "cursor-not-allowed"),
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
onPointerLeave: handlePointerUp,
style: {
width,
height
},
role: "application",
"aria-label": "Signature pad - draw your signature here",
"aria-describedby": `${signaturePadId}-instructions`
}), jsx("div", {
id: `${signaturePadId}-instructions`,
className: 'sr-only',
children: "Use your mouse or finger to draw your signature. Press clear to start over."
})]
}), (showClearButton || showSaveButton) && jsxs("div", {
className: "glass-flex glass-justify-between glass-items-center",
children: [jsx("div", {
className: "glass-flex glass-gap-2",
children: showClearButton && jsx("button", {
onClick: clear,
disabled: disabled || isEmpty,
className: cn("glass-px-4 glass-py-2 glass-radius-md glass-text-sm font-medium transition-all duration-200", "hover:scale-105 active:scale-95", "focus:outline-none focus:ring-2 focus:ring-primary/50", "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100", "glass-focus glass-touch-target glass-contrast-guard"),
children: jsx(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: "glass-w-full glass-h-full",
children: "Clear"
})
})
}), jsx("div", {
className: "glass-flex glass-gap-2",
children: showSaveButton && jsx("button", {
onClick: save,
disabled: disabled || isEmpty,
className: cn("glass-px-4 glass-py-2 glass-radius-md glass-text-sm font-medium transition-all duration-200", "hover:scale-105 active:scale-95", "focus:outline-none focus:ring-2 focus:ring-primary/50", "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100", "glass-focus glass-touch-target glass-contrast-guard"),
children: jsx(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "primary",
border: "subtle",
className: "glass-w-full glass-h-full",
children: "Save"
})
})
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between glass-text-sm glass-text-secondary",
children: [jsx("span", {
children: isEmpty ? "No signature" : "Signature captured"
}), isDrawing && jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("div", {
className: 'w-2 h-2 glass-surface-primary glass-radius-full animate-pulse'
}), jsx("span", {
children: "Drawing..."
})]
})]
})]
})
});
});
GlassSignaturePad.displayName = "GlassSignaturePad";
export { GlassSignaturePad, GlassSignaturePad as default };
//# sourceMappingURL=GlassSignaturePad.js.map