Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 165 additions & 127 deletions src/components/builder/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface Props {
onDuplicate?: (id: RatingType) => void;
onResetBadge: (id: LayerTargetId) => void;
onDelete: (id: LayerTargetId) => void;
isMobile?: boolean;
}

const ContextMenu: React.FC<Props> = memo(
Expand All @@ -72,6 +73,7 @@ const ContextMenu: React.FC<Props> = memo(
onDuplicate,
onResetBadge,
onDelete,
isMobile = false,
}) => {
const menuRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -145,11 +147,22 @@ const ContextMenu: React.FC<Props> = memo(
},
{
items: [
{ id: 'hide', label: isLogo ? 'Hide Layer' : 'Hide Badge', icon: <EyeOff size={12} />, shortcut: 'H' },
{
id: 'hide',
label: isLogo ? 'Hide Layer' : 'Hide Badge',
icon: <EyeOff size={12} />,
shortcut: 'H',
},
{ id: 'showall', label: 'Show All Badges', icon: <Eye size={12} /> },
{
id: 'select',
label: isSelected ? (isLogo ? 'Deselect Layer' : 'Deselect Badge') : isLogo ? 'Select Layer' : 'Select Badge',
label: isSelected
? isLogo
? 'Deselect Layer'
: 'Deselect Badge'
: isLogo
? 'Select Layer'
: 'Select Badge',
icon: isSelected ? <Square size={12} /> : <MousePointer2 size={12} />,
},
{ id: 'selectall', label: 'Select All', icon: <CheckSquare size={12} /> },
Expand All @@ -163,9 +176,16 @@ const ContextMenu: React.FC<Props> = memo(
},
{
items: [
...(!isLogo && onDuplicate ? [{ id: 'dup', label: 'Duplicate', icon: <Copy size={12} /> }] : []),
...(!isLogo && onDuplicate
? [{ id: 'dup', label: 'Duplicate', icon: <Copy size={12} /> }]
: []),
{ id: 'reset', label: 'Reset to Defaults', icon: <RotateCcw size={12} /> },
{ id: 'delete', label: isLogo ? 'Hide Logo Layer' : 'Delete Badge', icon: <Trash2 size={12} />, danger: true },
{
id: 'delete',
label: isLogo ? 'Hide Logo Layer' : 'Delete Badge',
icon: <Trash2 size={12} />,
danger: true,
},
],
},
];
Expand Down Expand Up @@ -200,7 +220,7 @@ const ContextMenu: React.FC<Props> = memo(
exec(() => onDeselectAll());
break;
case 'dup':
exec(() => onDuplicate?.(id));
if (id !== 'logo') exec(() => onDuplicate?.(id));
break;
case 'reset':
exec(() => onResetBadge(id));
Expand All @@ -212,146 +232,164 @@ const ContextMenu: React.FC<Props> = memo(
};

return (
<div
ref={menuRef}
role="menu"
aria-label="Badge context menu"
style={{
position: 'fixed',
top: pos.y,
left: pos.x,
zIndex: 9000,
minWidth: 200,
background: 'rgba(14,13,11,0.92)',
backdropFilter: 'blur(20px) saturate(1.4)',
WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
border: '1px solid rgba(196,124,46,0.2)',
borderRadius: 10,
boxShadow: '0 24px 64px rgba(0,0,0,0.7), 0 0 0 1px rgba(196,124,46,0.06)',
padding: '4px',
animation: 'ctx-enter 0.12s cubic-bezier(0.16,1,0.3,1)',
transformOrigin: 'top left',
}}
>
<style>{`
<>
{isMobile && (
<button
aria-label="Dismiss context menu"
onClick={onClose}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.5)',
border: 0,
zIndex: 8998,
}}
/>
)}
<div
ref={menuRef}
role="menu"
aria-label="Badge context menu"
style={{
position: 'fixed',
top: isMobile ? 'auto' : pos.y,
bottom: isMobile ? 'calc(64px + env(safe-area-inset-bottom, 0px) + 8px)' : 'auto',
left: isMobile ? 8 : pos.x,
right: isMobile ? 8 : 'auto',
zIndex: 9000,
minWidth: isMobile ? 0 : 200,
width: isMobile ? 'auto' : undefined,
background: 'rgba(14,13,11,0.92)',
backdropFilter: 'blur(20px) saturate(1.4)',
WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
border: '1px solid rgba(196,124,46,0.2)',
borderRadius: 10,
boxShadow: '0 24px 64px rgba(0,0,0,0.7), 0 0 0 1px rgba(196,124,46,0.06)',
padding: '4px',
animation: 'ctx-enter 0.12s cubic-bezier(0.16,1,0.3,1)',
transformOrigin: 'top left',
}}
>
<style>{`
@keyframes ctx-enter {
from { opacity: 0; transform: scale(0.95) translateY(-4px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
`}</style>

{/* Badge label header */}
<div
style={{
padding: '7px 10px 6px',
borderBottom: '1px solid rgba(255,255,255,0.06)',
marginBottom: 4,
display: 'flex',
alignItems: 'center',
gap: 7,
}}
>
{/* Badge label header */}
<div
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: 'var(--film-amber)',
boxShadow: '0 0 6px rgba(196,124,46,0.6)',
flexShrink: 0,
}}
/>
<span
style={{
fontSize: 10,
fontWeight: 700,
color: 'var(--film-amber)',
fontFamily: 'Syne, sans-serif',
letterSpacing: '0.08em',
textTransform: 'uppercase',
padding: '7px 10px 6px',
borderBottom: '1px solid rgba(255,255,255,0.06)',
marginBottom: 4,
display: 'flex',
alignItems: 'center',
gap: 7,
}}
>
{isLogo ? 'logo' : id}
</span>
{isSelected && (
<div
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: 'var(--film-amber)',
boxShadow: '0 0 6px rgba(196,124,46,0.6)',
flexShrink: 0,
}}
/>
<span
style={{
marginLeft: 'auto',
fontSize: 8,
color: 'rgba(196,124,46,0.5)',
fontFamily: 'JetBrains Mono, monospace',
letterSpacing: '0.1em',
fontSize: 10,
fontWeight: 700,
color: 'var(--film-amber)',
fontFamily: 'Syne, sans-serif',
letterSpacing: '0.08em',
textTransform: 'uppercase',
}}
>
selected
{isLogo ? 'logo' : id}
</span>
)}
</div>

{groups.map((group, gi) => (
<React.Fragment key={gi}>
{gi > 0 && (
<div style={{ height: 1, background: 'rgba(255,255,255,0.05)', margin: '4px 0' }} />
)}
{group.items.map((item) => (
<button
key={item.id}
role="menuitem"
onClick={() => handleAction(item.id)}
disabled={item.disabled}
{isSelected && (
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 9,
width: '100%',
padding: '7px 10px',
background: 'transparent',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
color: item.danger ? 'rgba(248,113,113,0.85)' : 'rgba(240,230,204,0.75)',
fontSize: 11,
fontFamily: 'Syne, sans-serif',
fontWeight: 500,
textAlign: 'left',
transition: 'background 0.1s, color 0.1s',
userSelect: 'none',
}}
onMouseEnter={(e) => {
const el = e.currentTarget;
el.style.background = item.danger
? 'rgba(248,113,113,0.1)'
: 'rgba(196,124,46,0.12)';
el.style.color = item.danger ? 'rgb(248,113,113)' : 'var(--film-cream)';
}}
onMouseLeave={(e) => {
const el = e.currentTarget;
el.style.background = 'transparent';
el.style.color = item.danger
? 'rgba(248,113,113,0.85)'
: 'rgba(240,230,204,0.75)';
marginLeft: 'auto',
fontSize: 8,
color: 'rgba(196,124,46,0.5)',
fontFamily: 'JetBrains Mono, monospace',
letterSpacing: '0.1em',
}}
>
<span style={{ opacity: 0.7, flexShrink: 0, lineHeight: 0 }}>{item.icon}</span>
<span style={{ flex: 1 }}>{item.label}</span>
{item.shortcut && (
<span
style={{
fontSize: 9,
color: 'var(--film-text-dim)',
fontFamily: 'JetBrains Mono, monospace',
letterSpacing: '0.05em',
flexShrink: 0,
}}
>
{item.shortcut}
</span>
)}
</button>
))}
</React.Fragment>
))}
</div>
selected
</span>
)}
</div>

{groups.map((group, gi) => (
<React.Fragment key={gi}>
{gi > 0 && (
<div style={{ height: 1, background: 'rgba(255,255,255,0.05)', margin: '4px 0' }} />
)}
{group.items.map((item) => (
<button
key={item.id}
role="menuitem"
onClick={() => handleAction(item.id)}
disabled={item.disabled}
style={{
display: 'flex',
alignItems: 'center',
gap: 9,
width: '100%',
padding: '7px 10px',
background: 'transparent',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
color: item.danger ? 'rgba(248,113,113,0.85)' : 'rgba(240,230,204,0.75)',
fontSize: 11,
fontFamily: 'Syne, sans-serif',
fontWeight: 500,
textAlign: 'left',
transition: 'background 0.1s, color 0.1s',
userSelect: 'none',
}}
onMouseEnter={(e) => {
const el = e.currentTarget;
el.style.background = item.danger
? 'rgba(248,113,113,0.1)'
: 'rgba(196,124,46,0.12)';
el.style.color = item.danger ? 'rgb(248,113,113)' : 'var(--film-cream)';
}}
onMouseLeave={(e) => {
const el = e.currentTarget;
el.style.background = 'transparent';
el.style.color = item.danger
? 'rgba(248,113,113,0.85)'
: 'rgba(240,230,204,0.75)';
}}
>
<span style={{ opacity: 0.7, flexShrink: 0, lineHeight: 0 }}>{item.icon}</span>
<span style={{ flex: 1 }}>{item.label}</span>
{item.shortcut && (
<span
style={{
fontSize: 9,
color: 'var(--film-text-dim)',
fontFamily: 'JetBrains Mono, monospace',
letterSpacing: '0.05em',
flexShrink: 0,
}}
>
{item.shortcut}
</span>
)}
</button>
))}
</React.Fragment>
))}
</div>
</>
);
}
);
Expand Down
Loading