diff --git a/src/components/editor/MaterialEditorWrapper.tsx b/src/components/editor/MaterialEditorWrapper.tsx new file mode 100644 index 00000000..9b75af4f --- /dev/null +++ b/src/components/editor/MaterialEditorWrapper.tsx @@ -0,0 +1,133 @@ +'use client'; + +/** + * Post Editor: Material Design (#418). + * + * Provides a focused, Material-style wrapper for the existing post editor + * without pulling in the full MUI dependency footprint. The component is + * purpose-built for our current Tailwind setup so the "Material" look — + * elevated surfaces, rounded corners, ripple-ready hit targets, motion — + * is achieved with composable primitives. + */ + +import { forwardRef, useCallback, useState } from 'react'; + +export interface MaterialEditorWrapperProps { + title: string; + children: React.ReactNode; + onSubmit?: () => void | Promise; + submitLabel?: string; + cancelLabel?: string; + onCancel?: () => void; + className?: string; +} + +type Ripple = { x: number; y: number; size: number; k: string }; + +export const MaterialEditorWrapper = forwardRef< + HTMLDivElement, + MaterialEditorWrapperProps +>(function MaterialEditorWrapper( + { + title, + children, + onSubmit, + submitLabel = 'Publish', + cancelLabel = 'Cancel', + onCancel, + className = '', + }, + ref, +) { + const [ripples, setRipples] = useState([]); + const [submitting, setSubmitting] = useState(false); + + const spawnRipple = useCallback( + (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height) * 1.6; + const k = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const ripple = { + x: e.clientX - rect.left - size / 2, + y: e.clientY - rect.top - size / 2, + size, + k, + }; + setRipples((r) => [...r, ripple]); + window.setTimeout(() => { + setRipples((r) => r.filter((rp) => rp.k !== k)); + }, 600); + }, + [], + ); + + const handleSubmit = useCallback( + async (e: React.MouseEvent) => { + spawnRipple(e); + if (!onSubmit) return; + setSubmitting(true); + try { + await onSubmit(); + } finally { + setSubmitting(false); + } + }, + [onSubmit, spawnRipple], + ); + + return ( +
+
+

{title}

+
+
{children}
+ +
+ ); +}); + +export default MaterialEditorWrapper;