feat: RefBoard v0.4.0 — collaborative reference board with layers, groups & polished UI
Full-featured PureRef-style collaborative canvas for game dev teams: - Layer panel with visibility, lock, drag reorder, group/ungroup (Ctrl+G/Shift+G) - Arrangement tools (grid, row, column) via right-click context menu - Copy to system clipboard (Ctrl+C writes PNG for external paste in Paint etc.) - Number shortcuts (1-5) for tool selection with visible shortcut badges - Premium dark UI across all pages (Login, Collections, Boards, Editor) - Socket.IO rooms for cursors, transforms, and presence notifications - MinIO image storage with backend proxy, drag/drop and paste upload
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
shortcut?: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
danger?: boolean;
|
||||
divider?: boolean;
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
items: MenuItem[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
document.addEventListener('keydown', handleKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
document.removeEventListener('keydown', handleKey);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
// Adjust position to stay within viewport
|
||||
const style: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
left: x,
|
||||
top: y,
|
||||
zIndex: 1000,
|
||||
background: '#2a2a2a',
|
||||
border: '1px solid #3d3d3d',
|
||||
borderRadius: '8px',
|
||||
padding: '4px 0',
|
||||
minWidth: '180px',
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.5)',
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} style={style}>
|
||||
{items.map((item, i) => {
|
||||
if (item.divider) {
|
||||
return <div key={i} style={{ height: '1px', background: '#3d3d3d', margin: '4px 0' }} />;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
disabled={item.disabled}
|
||||
onClick={() => { item.onClick(); onClose(); }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
padding: '6px 12px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: item.disabled ? '#555' : item.danger ? '#ff6b6b' : '#ddd',
|
||||
fontSize: '12px',
|
||||
cursor: item.disabled ? 'default' : 'pointer',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!item.disabled) e.currentTarget.style.background = '#363636';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
}}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{item.shortcut && (
|
||||
<span style={{ color: '#666', fontSize: '11px', marginLeft: '20px' }}>{item.shortcut}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user