Skip to content

Commit 6295734

Browse files
committed
feat(editor): restore the dialogs, context menus and gradient tools
1 parent 0fe7db7 commit 6295734

15 files changed

Lines changed: 673 additions & 94 deletions
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
export interface ContextMenuEntry {
4+
label: string;
5+
shortcut?: string;
6+
danger?: boolean;
7+
disabled?: boolean;
8+
onSelect: () => void;
9+
}
10+
11+
interface ContextMenuProps {
12+
x: number;
13+
y: number;
14+
entries: (ContextMenuEntry | 'divider')[];
15+
onDismiss: () => void;
16+
}
17+
18+
/** The editor's right click menu, dismissed by Escape or a click outside. */
19+
export function ContextMenu({ x, y, entries, onDismiss }: ContextMenuProps) {
20+
const menu = useRef<HTMLDivElement>(null);
21+
22+
useEffect(() => {
23+
const dismiss = (event: MouseEvent): void => {
24+
if (menu.current !== null && !menu.current.contains(event.target as Node)) {
25+
onDismiss();
26+
}
27+
};
28+
29+
const escape = (event: KeyboardEvent): void => {
30+
if (event.key === 'Escape') {
31+
onDismiss();
32+
}
33+
};
34+
35+
document.addEventListener('mousedown', dismiss);
36+
document.addEventListener('keydown', escape);
37+
38+
return () => {
39+
document.removeEventListener('mousedown', dismiss);
40+
document.removeEventListener('keydown', escape);
41+
};
42+
}, [onDismiss]);
43+
44+
return (
45+
<div ref={menu} role="menu" style={{ left: x, top: y }} className="context-menu">
46+
{entries.map((entry, index) =>
47+
entry === 'divider' ? (
48+
<div key={`divider-${index}`} className="context-menu-divider" />
49+
) : (
50+
<button
51+
key={entry.label}
52+
type="button"
53+
role="menuitem"
54+
disabled={entry.disabled}
55+
onClick={() => {
56+
entry.onSelect();
57+
onDismiss();
58+
}}
59+
className={`context-menu-item${entry.danger === true ? ' danger' : ''}`}
60+
>
61+
{entry.label}
62+
{entry.shortcut !== undefined && <span className="shortcut">{entry.shortcut}</span>}
63+
</button>
64+
),
65+
)}
66+
</div>
67+
);
68+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useState } from 'react';
2+
import { Modal } from './Modal';
3+
import type { MenuDescriptor } from '../types/editor';
4+
5+
interface DeleteMenuDialogProps {
6+
menu: MenuDescriptor;
7+
onConfirm: () => void;
8+
onClose: () => void;
9+
}
10+
11+
/**
12+
* Deleting asks twice, as it did before the migration: the file is removed from
13+
* the server and there is no undo.
14+
*/
15+
export function DeleteMenuDialog({ menu, onConfirm, onClose }: DeleteMenuDialogProps) {
16+
const [confirming, setConfirming] = useState(false);
17+
18+
if (!confirming) {
19+
return (
20+
<Modal
21+
title="⚠️ Confirm Deletion"
22+
onClose={onClose}
23+
footer={
24+
<>
25+
<button type="button" className="btn btn-secondary" onClick={onClose}>
26+
Cancel
27+
</button>
28+
<button type="button" className="btn btn-danger" onClick={() => setConfirming(true)}>
29+
Delete
30+
</button>
31+
</>
32+
}
33+
>
34+
<p style={{ color: 'var(--error-color)', fontWeight: 'bold', marginBottom: '12px' }}>
35+
WARNING! This action is irreversible.
36+
</p>
37+
<p style={{ marginBottom: '8px' }}>You are about to delete the menu:</p>
38+
<p
39+
style={{
40+
background: 'var(--bg-secondary)',
41+
padding: '8px',
42+
borderRadius: '4px',
43+
fontFamily: 'monospace',
44+
marginBottom: '12px',
45+
}}
46+
>
47+
📄 {menu.fileName}
48+
</p>
49+
<p style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>
50+
The file will be permanently deleted from the server.
51+
</p>
52+
</Modal>
53+
);
54+
}
55+
56+
return (
57+
<Modal
58+
title="⚠️ Final Confirmation"
59+
onClose={onClose}
60+
footer={
61+
<>
62+
<button type="button" className="btn btn-secondary" onClick={onClose}>
63+
Cancel
64+
</button>
65+
<button type="button" className="btn btn-danger" onClick={onConfirm}>
66+
Delete permanently
67+
</button>
68+
</>
69+
}
70+
>
71+
<p style={{ color: 'var(--error-color)', fontWeight: 'bold', marginBottom: '12px' }}>FINAL WARNING!</p>
72+
<p>
73+
<code>{menu.fileName}</code> will be deleted from the server and cannot be recovered.
74+
</p>
75+
</Modal>
76+
);
77+
}

resources/js/components/DemoWorkspace.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@ export function DemoWorkspace() {
5555
busy={false}
5656
canSave={false}
5757
onToggleVisual={() => setVisualMode(mode => !mode)}
58-
onValidate={() =>
59-
current !== null && setDiagnostics(validateMenuSource(current.content, current.menu.platform))
58+
onRefresh={() => notify('info', 'The demo always shows the sample menus.')}
59+
onToggleOutput={() =>
60+
setDiagnostics(current === null || diagnostics !== null
61+
? null
62+
: validateMenuSource(current.content, current.menu.platform))
6063
}
6164
onSave={() => notify('info', 'The demo cannot save. Open the editor from your server to keep changes.')}
6265
onOpen={menu => void openMenu(menu)}
@@ -65,6 +68,7 @@ export function DemoWorkspace() {
6568
statusLeft={`${store.menus.filter(menu => menu.platform !== 'CONFIG').length} menus loaded`}
6669
statusRight={EDITOR_VERSION}
6770
onCloseDiagnostics={() => setDiagnostics(null)}
71+
onReloadTab={() => notify('info', 'The demo always shows the sample menus.')}
6872
/>
6973
);
7074
}

resources/js/components/EditorTabs.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
import { useState, type ReactNode } from 'react';
22
import { isDirty, type OpenTab } from '../editor/store';
3+
import { ContextMenu } from './ContextMenu';
34

45
interface EditorTabsProps {
56
tabs: OpenTab[];
67
activeKey: string | null;
78
actions?: ReactNode;
89
onActivate: (key: string) => void;
910
onClose: (key: string) => void;
11+
onCloseOthers: (key: string) => void;
12+
onCloseAll: () => void;
13+
onReload: (key: string) => void;
1014
onReorder: (from: number, to: number) => void;
1115
}
1216

13-
export function EditorTabs({ tabs, activeKey, actions, onActivate, onClose, onReorder }: EditorTabsProps) {
17+
export function EditorTabs({
18+
tabs,
19+
activeKey,
20+
actions,
21+
onActivate,
22+
onClose,
23+
onCloseOthers,
24+
onCloseAll,
25+
onReload,
26+
onReorder,
27+
}: EditorTabsProps) {
1428
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
1529
const [overIndex, setOverIndex] = useState<number | null>(null);
30+
const [menu, setMenu] = useState<{ key: string; x: number; y: number } | null>(null);
1631

1732
return (
1833
<div className="editor-tabs">
@@ -42,6 +57,10 @@ export function EditorTabs({ tabs, activeKey, actions, onActivate, onClose, onRe
4257
}}
4358
onClick={() => onActivate(tab.key)}
4459
onKeyDown={event => event.key === 'Enter' && onActivate(tab.key)}
60+
onContextMenu={event => {
61+
event.preventDefault();
62+
setMenu({ key: tab.key, x: event.clientX, y: event.clientY });
63+
}}
4564
className={[
4665
'editor-tab',
4766
tab.key === activeKey ? 'active' : '',
@@ -69,6 +88,21 @@ export function EditorTabs({ tabs, activeKey, actions, onActivate, onClose, onRe
6988
</div>
7089

7190
{actions !== undefined && <div className="tabs-actions">{actions}</div>}
91+
92+
{menu !== null && (
93+
<ContextMenu
94+
x={menu.x}
95+
y={menu.y}
96+
onDismiss={() => setMenu(null)}
97+
entries={[
98+
{ label: 'Reload from server', onSelect: () => onReload(menu.key) },
99+
'divider',
100+
{ label: 'Close', onSelect: () => onClose(menu.key) },
101+
{ label: 'Close others', disabled: tabs.length < 2, onSelect: () => onCloseOthers(menu.key) },
102+
{ label: 'Close all', onSelect: onCloseAll },
103+
]}
104+
/>
105+
)}
72106
</div>
73107
);
74108
}

0 commit comments

Comments
 (0)