babylon.quarks-editor 0.17.7 → 0.17.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/core/timeline.d.ts +1 -1
  2. package/dist/core/timeline.js +3 -2
  3. package/dist/react/EffectEditor.d.ts +3 -0
  4. package/dist/react/EffectEditor.js +6 -1
  5. package/dist/react/EffectEditorHost.js +72 -11
  6. package/dist/react/ObjectModule.d.ts +6 -0
  7. package/dist/react/ObjectModule.js +28 -0
  8. package/dist/react/TimelinePanel.d.ts +4 -3
  9. package/dist/react/TimelinePanel.js +60 -10
  10. package/dist/react/TimelineTrackRow.d.ts +2 -2
  11. package/dist/react/TimelineTrackRow.js +16 -12
  12. package/dist/react/theme.js +44 -21
  13. package/package.json +68 -63
  14. package/src/core/binding.ts +105 -105
  15. package/src/core/collision.ts +40 -40
  16. package/src/core/groups.ts +29 -29
  17. package/src/core/loadEffect.ts +102 -102
  18. package/src/core/scrub.ts +28 -28
  19. package/src/core/shapes.ts +133 -133
  20. package/src/core/systems.ts +98 -98
  21. package/src/core/timeline.ts +95 -94
  22. package/src/core/values.ts +113 -113
  23. package/src/index.ts +32 -32
  24. package/src/react/CurveEditor.tsx +168 -168
  25. package/src/react/EffectEditor.tsx +125 -111
  26. package/src/react/EffectEditorHost.tsx +466 -377
  27. package/src/react/GradientEditor.tsx +127 -127
  28. package/src/react/ModuleSection.tsx +54 -54
  29. package/src/react/ObjectModule.tsx +47 -0
  30. package/src/react/PromptDialog.tsx +85 -85
  31. package/src/react/TimelinePanel.tsx +412 -354
  32. package/src/react/TimelineTrackRow.tsx +291 -267
  33. package/src/react/behaviorModules.tsx +658 -658
  34. package/src/react/fields.tsx +76 -76
  35. package/src/react/index.ts +43 -43
  36. package/src/react/modules.tsx +1034 -1034
  37. package/src/react/theme.ts +108 -85
@@ -1,127 +1,127 @@
1
- import React, {useRef, useState} from 'react';
2
- import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
3
- import {NumberField, Row} from './fields';
4
- import {buttonStyle, theme} from './theme';
5
-
6
- const BAR_WIDTH = 218;
7
-
8
- /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
9
- export function GradientEditor(props: {stops: GradientStop[]; onChange: (stops: GradientStop[]) => void}) {
10
- const [selected, setSelected] = useState(0);
11
- const barRef = useRef<HTMLDivElement>(null);
12
- const dragging = useRef(false);
13
-
14
- const stops = props.stops;
15
- const current = stops[Math.min(selected, stops.length - 1)];
16
-
17
- const posFromEvent = (clientX: number) => {
18
- const rect = barRef.current!.getBoundingClientRect();
19
- return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
20
- };
21
-
22
- const updateStop = (index: number, patch: Partial<GradientStop>) => {
23
- const next = stops.map((s, i) => (i === index ? {...s, ...patch} : s));
24
- props.onChange(next);
25
- };
26
-
27
- return (
28
- <div>
29
- <div
30
- ref={barRef}
31
- className="qe-hover"
32
- style={{
33
- position: 'relative',
34
- width: BAR_WIDTH,
35
- height: 20,
36
- borderRadius: 6,
37
- border: `1px solid ${theme.border}`,
38
- background: `${stopsToCssGradient(stops)}, repeating-conic-gradient(#333 0% 25%, #555 0% 50%) 0 0 / 10px 10px`,
39
- cursor: 'copy',
40
- }}
41
- onPointerDown={(e) => {
42
- if (e.target !== barRef.current) {
43
- return;
44
- }
45
- const pos = posFromEvent(e.clientX);
46
- const base = current ?? {r: 1, g: 1, b: 1, a: 1, pos: 0};
47
- const next = [...stops, {...base, pos}];
48
- props.onChange(next);
49
- setSelected(next.length - 1);
50
- }}
51
- onPointerMove={(e) => {
52
- if (dragging.current) {
53
- updateStop(selected, {pos: posFromEvent(e.clientX)});
54
- }
55
- }}
56
- onPointerUp={() => {
57
- dragging.current = false;
58
- }}
59
- >
60
- {stops.map((stop, i) => (
61
- <div
62
- key={i}
63
- className="qe-hover"
64
- style={{
65
- position: 'absolute',
66
- left: `calc(${stop.pos * 100}% - 6px)`,
67
- top: -4,
68
- width: 12,
69
- height: 26,
70
- borderRadius: 4,
71
- background: rgbToHex(stop.r, stop.g, stop.b),
72
- border: `2px solid ${i === selected ? theme.accent : '#0a1020'}`,
73
- cursor: 'ew-resize',
74
- boxSizing: 'border-box',
75
- }}
76
- onPointerDown={(e) => {
77
- e.stopPropagation();
78
- setSelected(i);
79
- dragging.current = true;
80
- (e.currentTarget as Element).setPointerCapture(e.pointerId);
81
- }}
82
- onPointerMove={(e) => {
83
- if (dragging.current && i === selected) {
84
- updateStop(i, {pos: posFromEvent(e.clientX)});
85
- }
86
- }}
87
- onPointerUp={(e) => {
88
- dragging.current = false;
89
- (e.currentTarget as Element).releasePointerCapture(e.pointerId);
90
- }}
91
- />
92
- ))}
93
- </div>
94
- {current && (
95
- <>
96
- <Row label="Stop color">
97
- <input
98
- type="color"
99
- className="qe-hover"
100
- value={rgbToHex(current.r, current.g, current.b)}
101
- onChange={(e) => updateStop(selected, hexToRgb(e.target.value))}
102
- style={{width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer'}}
103
- />
104
- </Row>
105
- <Row label="Stop alpha">
106
- <NumberField value={current.a} min={0} step={0.05} onChange={(a) => updateStop(selected, {a: Math.min(1, a)})} />
107
- </Row>
108
- <Row label="Stop position">
109
- <NumberField value={current.pos} min={0} step={0.05} onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})} />
110
- </Row>
111
- {stops.length > 2 && (
112
- <button
113
- className="qe-hover"
114
- style={{...buttonStyle, marginTop: 6}}
115
- onClick={() => {
116
- props.onChange(stops.filter((_, i) => i !== selected));
117
- setSelected(0);
118
- }}
119
- >
120
- Remove stop
121
- </button>
122
- )}
123
- </>
124
- )}
125
- </div>
126
- );
127
- }
1
+ import React, {useRef, useState} from 'react';
2
+ import {GradientStop, hexToRgb, rgbToHex, stopsToCssGradient} from '../core/colors';
3
+ import {NumberField, Row} from './fields';
4
+ import {buttonStyle, theme} from './theme';
5
+
6
+ const BAR_WIDTH = 218;
7
+
8
+ /** Gradient editor: RGBA stops on a bar — click to add, drag to move, edit the selected stop. */
9
+ export function GradientEditor(props: {stops: GradientStop[]; onChange: (stops: GradientStop[]) => void}) {
10
+ const [selected, setSelected] = useState(0);
11
+ const barRef = useRef<HTMLDivElement>(null);
12
+ const dragging = useRef(false);
13
+
14
+ const stops = props.stops;
15
+ const current = stops[Math.min(selected, stops.length - 1)];
16
+
17
+ const posFromEvent = (clientX: number) => {
18
+ const rect = barRef.current!.getBoundingClientRect();
19
+ return Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
20
+ };
21
+
22
+ const updateStop = (index: number, patch: Partial<GradientStop>) => {
23
+ const next = stops.map((s, i) => (i === index ? {...s, ...patch} : s));
24
+ props.onChange(next);
25
+ };
26
+
27
+ return (
28
+ <div>
29
+ <div
30
+ ref={barRef}
31
+ className="qe-hover"
32
+ style={{
33
+ position: 'relative',
34
+ width: BAR_WIDTH,
35
+ height: 20,
36
+ borderRadius: 6,
37
+ border: `1px solid ${theme.border}`,
38
+ background: `${stopsToCssGradient(stops)}, repeating-conic-gradient(#333 0% 25%, #555 0% 50%) 0 0 / 10px 10px`,
39
+ cursor: 'copy',
40
+ }}
41
+ onPointerDown={(e) => {
42
+ if (e.target !== barRef.current) {
43
+ return;
44
+ }
45
+ const pos = posFromEvent(e.clientX);
46
+ const base = current ?? {r: 1, g: 1, b: 1, a: 1, pos: 0};
47
+ const next = [...stops, {...base, pos}];
48
+ props.onChange(next);
49
+ setSelected(next.length - 1);
50
+ }}
51
+ onPointerMove={(e) => {
52
+ if (dragging.current) {
53
+ updateStop(selected, {pos: posFromEvent(e.clientX)});
54
+ }
55
+ }}
56
+ onPointerUp={() => {
57
+ dragging.current = false;
58
+ }}
59
+ >
60
+ {stops.map((stop, i) => (
61
+ <div
62
+ key={i}
63
+ className="qe-hover"
64
+ style={{
65
+ position: 'absolute',
66
+ left: `calc(${stop.pos * 100}% - 6px)`,
67
+ top: -4,
68
+ width: 12,
69
+ height: 26,
70
+ borderRadius: 4,
71
+ background: rgbToHex(stop.r, stop.g, stop.b),
72
+ border: `2px solid ${i === selected ? theme.accent : '#0a1020'}`,
73
+ cursor: 'ew-resize',
74
+ boxSizing: 'border-box',
75
+ }}
76
+ onPointerDown={(e) => {
77
+ e.stopPropagation();
78
+ setSelected(i);
79
+ dragging.current = true;
80
+ (e.currentTarget as Element).setPointerCapture(e.pointerId);
81
+ }}
82
+ onPointerMove={(e) => {
83
+ if (dragging.current && i === selected) {
84
+ updateStop(i, {pos: posFromEvent(e.clientX)});
85
+ }
86
+ }}
87
+ onPointerUp={(e) => {
88
+ dragging.current = false;
89
+ (e.currentTarget as Element).releasePointerCapture(e.pointerId);
90
+ }}
91
+ />
92
+ ))}
93
+ </div>
94
+ {current && (
95
+ <>
96
+ <Row label="Stop color">
97
+ <input
98
+ type="color"
99
+ className="qe-hover"
100
+ value={rgbToHex(current.r, current.g, current.b)}
101
+ onChange={(e) => updateStop(selected, hexToRgb(e.target.value))}
102
+ style={{width: 42, height: 24, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer'}}
103
+ />
104
+ </Row>
105
+ <Row label="Stop alpha">
106
+ <NumberField value={current.a} min={0} step={0.05} onChange={(a) => updateStop(selected, {a: Math.min(1, a)})} />
107
+ </Row>
108
+ <Row label="Stop position">
109
+ <NumberField value={current.pos} min={0} step={0.05} onChange={(pos) => updateStop(selected, {pos: Math.min(1, pos)})} />
110
+ </Row>
111
+ {stops.length > 2 && (
112
+ <button
113
+ className="qe-hover"
114
+ style={{...buttonStyle, marginTop: 6}}
115
+ onClick={() => {
116
+ props.onChange(stops.filter((_, i) => i !== selected));
117
+ setSelected(0);
118
+ }}
119
+ >
120
+ Remove stop
121
+ </button>
122
+ )}
123
+ </>
124
+ )}
125
+ </div>
126
+ );
127
+ }
@@ -1,54 +1,54 @@
1
- import React, {useState} from 'react';
2
- import {theme} from './theme';
3
-
4
- /** Unity-style module foldout with an optional enable checkbox. */
5
- export function ModuleSection(props: {
6
- title: string;
7
- children: React.ReactNode;
8
- enabled?: boolean;
9
- onToggle?: (enabled: boolean) => void;
10
- defaultOpen?: boolean;
11
- }) {
12
- const [open, setOpen] = useState(props.defaultOpen ?? true);
13
- const toggleable = props.onToggle !== undefined;
14
- const dimmed = toggleable && props.enabled === false;
15
- return (
16
- <section
17
- style={{
18
- border: `1px solid ${theme.border}`,
19
- borderRadius: 10,
20
- background: theme.sectionBg,
21
- marginTop: 8,
22
- overflow: 'hidden',
23
- }}
24
- >
25
- <header
26
- className="qe-hover"
27
- style={{
28
- display: 'flex',
29
- alignItems: 'center',
30
- gap: 8,
31
- padding: '7px 10px',
32
- cursor: 'pointer',
33
- userSelect: 'none',
34
- background: 'rgba(20, 30, 60, 0.5)',
35
- }}
36
- onClick={() => setOpen(!open)}
37
- >
38
- <span style={{color: theme.accent, fontSize: 10, width: 10}}>{open ? '▼' : '►'}</span>
39
- {toggleable && (
40
- <input
41
- type="checkbox"
42
- className="qe-hover"
43
- checked={props.enabled}
44
- onClick={(e) => e.stopPropagation()}
45
- onChange={(e) => props.onToggle?.(e.target.checked)}
46
- style={{width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer'}}
47
- />
48
- )}
49
- <span style={{color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600}}>{props.title}</span>
50
- </header>
51
- {open && !dimmed && <div style={{padding: '4px 10px 10px'}}>{props.children}</div>}
52
- </section>
53
- );
54
- }
1
+ import React, {useState} from 'react';
2
+ import {theme} from './theme';
3
+
4
+ /** Unity-style module foldout with an optional enable checkbox. */
5
+ export function ModuleSection(props: {
6
+ title: string;
7
+ children: React.ReactNode;
8
+ enabled?: boolean;
9
+ onToggle?: (enabled: boolean) => void;
10
+ defaultOpen?: boolean;
11
+ }) {
12
+ const [open, setOpen] = useState(props.defaultOpen ?? true);
13
+ const toggleable = props.onToggle !== undefined;
14
+ const dimmed = toggleable && props.enabled === false;
15
+ return (
16
+ <section
17
+ style={{
18
+ border: `1px solid ${theme.border}`,
19
+ borderRadius: 10,
20
+ background: theme.sectionBg,
21
+ marginTop: 8,
22
+ overflow: 'hidden',
23
+ }}
24
+ >
25
+ <header
26
+ className="qe-hover"
27
+ style={{
28
+ display: 'flex',
29
+ alignItems: 'center',
30
+ gap: 8,
31
+ padding: '7px 10px',
32
+ cursor: 'pointer',
33
+ userSelect: 'none',
34
+ background: 'rgba(20, 30, 60, 0.5)',
35
+ }}
36
+ onClick={() => setOpen(!open)}
37
+ >
38
+ <span style={{color: theme.accent, fontSize: 10, width: 10}}>{open ? '▼' : '►'}</span>
39
+ {toggleable && (
40
+ <input
41
+ type="checkbox"
42
+ className="qe-hover"
43
+ checked={props.enabled}
44
+ onClick={(e) => e.stopPropagation()}
45
+ onChange={(e) => props.onToggle?.(e.target.checked)}
46
+ style={{width: 14, height: 14, accentColor: '#78a5ff', cursor: 'pointer'}}
47
+ />
48
+ )}
49
+ <span style={{color: dimmed ? theme.textDim : theme.text, fontSize: 13, fontWeight: 600}}>{props.title}</span>
50
+ </header>
51
+ {open && !dimmed && <div style={{padding: '4px 10px 10px'}}>{props.children}</div>}
52
+ </section>
53
+ );
54
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
3
+ import {Quaternion} from '@babylonjs/core/Maths/math.vector';
4
+ import {ModuleSection} from './ModuleSection';
5
+ import {NumberField, Row} from './fields';
6
+
7
+ const RAD_TO_DEG = 180 / Math.PI;
8
+ const DEG_TO_RAD = Math.PI / 180;
9
+
10
+ function getEulerDegrees(node: TransformNode): {x: number; y: number; z: number} {
11
+ const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation;
12
+ return {x: euler.x * RAD_TO_DEG, y: euler.y * RAD_TO_DEG, z: euler.z * RAD_TO_DEG};
13
+ }
14
+
15
+ function setEulerDegreeAxis(node: TransformNode, axis: 'x' | 'y' | 'z', degrees: number): void {
16
+ const euler = node.rotationQuaternion ? node.rotationQuaternion.toEulerAngles() : node.rotation.clone();
17
+ euler[axis] = degrees * DEG_TO_RAD;
18
+ if (node.rotationQuaternion) {
19
+ node.rotationQuaternion = Quaternion.FromEulerVector(euler);
20
+ } else {
21
+ node.rotation = euler;
22
+ }
23
+ }
24
+
25
+ function AxisRow(props: {label: string; values: {x: number; y: number; z: number}; step: number; onChange: (axis: 'x' | 'y' | 'z', value: number) => void}) {
26
+ return (
27
+ <Row label={props.label}>
28
+ <div style={{display: 'flex', gap: 4}}>
29
+ <NumberField value={props.values.x} step={props.step} onChange={(v) => props.onChange('x', v)} />
30
+ <NumberField value={props.values.y} step={props.step} onChange={(v) => props.onChange('y', v)} />
31
+ <NumberField value={props.values.z} step={props.step} onChange={(v) => props.onChange('z', v)} />
32
+ </div>
33
+ </Row>
34
+ );
35
+ }
36
+
37
+ export function ObjectModule(props: {node: TransformNode; label: string}) {
38
+ const {node} = props;
39
+ const euler = getEulerDegrees(node);
40
+ return (
41
+ <ModuleSection title={`Object — ${props.label}`}>
42
+ <AxisRow label="Position" values={{x: node.position.x, y: node.position.y, z: node.position.z}} step={0.1} onChange={(axis, v) => (node.position[axis] = v)} />
43
+ <AxisRow label="Rotation" values={euler} step={1} onChange={(axis, v) => setEulerDegreeAxis(node, axis, v)} />
44
+ <AxisRow label="Scale" values={{x: node.scaling.x, y: node.scaling.y, z: node.scaling.z}} step={0.1} onChange={(axis, v) => (node.scaling[axis] = v)} />
45
+ </ModuleSection>
46
+ );
47
+ }
@@ -1,85 +1,85 @@
1
- import React, {useEffect, useRef, useState} from 'react';
2
- import {buttonStyle, inputStyle, theme} from './theme';
3
-
4
- export interface PromptDialogProps {
5
- title: string;
6
- defaultValue?: string;
7
- placeholder?: string;
8
- confirmLabel?: string;
9
- /** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
10
- allowEmpty?: boolean;
11
- onSubmit: (value: string) => void;
12
- onCancel: () => void;
13
- }
14
-
15
- /** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
16
- * Enter, cancels on Escape or backdrop click. */
17
- export function PromptDialog(props: PromptDialogProps): React.ReactElement {
18
- const {title, defaultValue = '', placeholder, confirmLabel = 'OK', allowEmpty = false, onSubmit, onCancel} = props;
19
- const [value, setValue] = useState(defaultValue);
20
- const inputRef = useRef<HTMLInputElement>(null);
21
-
22
- useEffect(() => {
23
- inputRef.current?.focus();
24
- inputRef.current?.select();
25
- }, []);
26
-
27
- const submit = () => {
28
- const trimmed = value.trim();
29
- if (trimmed || allowEmpty) {
30
- onSubmit(trimmed);
31
- } else {
32
- onCancel();
33
- }
34
- };
35
-
36
- return (
37
- <div
38
- style={{
39
- position: 'fixed',
40
- inset: 0,
41
- background: 'rgba(3, 5, 12, 0.6)',
42
- display: 'flex',
43
- alignItems: 'center',
44
- justifyContent: 'center',
45
- zIndex: 1000,
46
- fontFamily: theme.font,
47
- }}
48
- onClick={onCancel}
49
- >
50
- <div
51
- style={{
52
- background: theme.panelBg,
53
- border: `1px solid ${theme.border}`,
54
- borderRadius: 10,
55
- padding: 16,
56
- minWidth: 300,
57
- boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
58
- }}
59
- onClick={(e) => e.stopPropagation()}
60
- >
61
- <div style={{fontSize: 13, color: theme.text, marginBottom: 10}}>{title}</div>
62
- <input
63
- ref={inputRef}
64
- className="qe-hover"
65
- style={{...inputStyle, marginBottom: 14}}
66
- value={value}
67
- placeholder={placeholder}
68
- onChange={(e) => setValue(e.target.value)}
69
- onKeyDown={(e) => {
70
- if (e.key === 'Enter') submit();
71
- if (e.key === 'Escape') onCancel();
72
- }}
73
- />
74
- <div style={{display: 'flex', justifyContent: 'flex-end', gap: 8}}>
75
- <button className="qe-hover" style={buttonStyle} onClick={onCancel}>
76
- Cancel
77
- </button>
78
- <button className="qe-hover" style={{...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent}} onClick={submit}>
79
- {confirmLabel}
80
- </button>
81
- </div>
82
- </div>
83
- </div>
84
- );
85
- }
1
+ import React, {useEffect, useRef, useState} from 'react';
2
+ import {buttonStyle, inputStyle, theme} from './theme';
3
+
4
+ export interface PromptDialogProps {
5
+ title: string;
6
+ defaultValue?: string;
7
+ placeholder?: string;
8
+ confirmLabel?: string;
9
+ /** Rejects empty/whitespace-only input by default (matches the old prompt() behavior). */
10
+ allowEmpty?: boolean;
11
+ onSubmit: (value: string) => void;
12
+ onCancel: () => void;
13
+ }
14
+
15
+ /** Modal replacement for `window.prompt` — matches the editor's dark theme, submits on
16
+ * Enter, cancels on Escape or backdrop click. */
17
+ export function PromptDialog(props: PromptDialogProps): React.ReactElement {
18
+ const {title, defaultValue = '', placeholder, confirmLabel = 'OK', allowEmpty = false, onSubmit, onCancel} = props;
19
+ const [value, setValue] = useState(defaultValue);
20
+ const inputRef = useRef<HTMLInputElement>(null);
21
+
22
+ useEffect(() => {
23
+ inputRef.current?.focus();
24
+ inputRef.current?.select();
25
+ }, []);
26
+
27
+ const submit = () => {
28
+ const trimmed = value.trim();
29
+ if (trimmed || allowEmpty) {
30
+ onSubmit(trimmed);
31
+ } else {
32
+ onCancel();
33
+ }
34
+ };
35
+
36
+ return (
37
+ <div
38
+ style={{
39
+ position: 'fixed',
40
+ inset: 0,
41
+ background: 'rgba(3, 5, 12, 0.6)',
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ justifyContent: 'center',
45
+ zIndex: 1000,
46
+ fontFamily: theme.font,
47
+ }}
48
+ onClick={onCancel}
49
+ >
50
+ <div
51
+ style={{
52
+ background: theme.panelBg,
53
+ border: `1px solid ${theme.border}`,
54
+ borderRadius: 10,
55
+ padding: 16,
56
+ minWidth: 300,
57
+ boxShadow: '0 12px 32px rgba(0, 0, 0, 0.5)',
58
+ }}
59
+ onClick={(e) => e.stopPropagation()}
60
+ >
61
+ <div style={{fontSize: 13, color: theme.text, marginBottom: 10}}>{title}</div>
62
+ <input
63
+ ref={inputRef}
64
+ className="qe-hover"
65
+ style={{...inputStyle, marginBottom: 14}}
66
+ value={value}
67
+ placeholder={placeholder}
68
+ onChange={(e) => setValue(e.target.value)}
69
+ onKeyDown={(e) => {
70
+ if (e.key === 'Enter') submit();
71
+ if (e.key === 'Escape') onCancel();
72
+ }}
73
+ />
74
+ <div style={{display: 'flex', justifyContent: 'flex-end', gap: 8}}>
75
+ <button className="qe-hover" style={buttonStyle} onClick={onCancel}>
76
+ Cancel
77
+ </button>
78
+ <button className="qe-hover" style={{...buttonStyle, background: theme.accent, color: '#0a1128', borderColor: theme.accent}} onClick={submit}>
79
+ {confirmLabel}
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ }