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,267 +1,291 @@
1
- import React, {useState} from 'react';
2
- import type {ParticleSystem} from 'babylon.quarks';
3
- import type {EffectBinding} from '../core/binding';
4
- import {buildEffectTree, collectSystems} from '../core/effectTree';
5
- import {createChildSystem, removeSystems} from '../core/systems';
6
- import {ungroupNode} from '../core/groups';
7
- import type {TimelineRow} from '../core/timeline';
8
- import {PromptDialog} from './PromptDialog';
9
- import {theme} from './theme';
10
-
11
- const iconButton: React.CSSProperties = {background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, padding: 0};
12
-
13
- const ROW_HEIGHT = 28;
14
-
15
- export interface TimelineTrackRowProps {
16
- row: TimelineRow;
17
- binding: EffectBinding;
18
- selected: boolean;
19
- onSelect: (system: ParticleSystem) => void;
20
- pxPerSecond: number;
21
- /** Matches the ruler's left gutter so bars line up under the correct tick. */
22
- offsetPx: number;
23
- collapsed: boolean;
24
- onToggleCollapse?: () => void;
25
- /** Preview-only visibility (toggles `system.emitter.visible`); lets you isolate one track. */
26
- hidden?: boolean;
27
- onToggleVisible?: () => void;
28
- /** Membership in the "group selected rows together" set (unrelated to inspector selection). */
29
- groupSelected: boolean;
30
- onToggleGroupSelect: () => void;
31
- }
32
-
33
- const checkboxButton: React.CSSProperties = {...iconButton, fontSize: 12, width: 12};
34
-
35
- /** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
36
- export function TimelineTrackRow(props: TimelineTrackRowProps): React.ReactElement {
37
- const {row, binding, selected, onSelect, pxPerSecond, offsetPx, collapsed, onToggleCollapse, hidden, onToggleVisible, groupSelected, onToggleGroupSelect} = props;
38
- const [renaming, setRenaming] = useState(false);
39
-
40
- const isRoot = row.node === binding.root;
41
-
42
- const labelStyle: React.CSSProperties = {
43
- display: 'flex',
44
- alignItems: 'center',
45
- gap: 6,
46
- height: ROW_HEIGHT,
47
- padding: '0 8px',
48
- paddingLeft: 8 + row.depth * 14,
49
- borderRadius: 6,
50
- cursor: row.kind === 'track' ? 'pointer' : onToggleCollapse ? 'pointer' : 'default',
51
- fontSize: 12.5,
52
- color: row.kind === 'track' && selected ? theme.text : theme.textDim,
53
- whiteSpace: 'nowrap',
54
- // Left unset (rather than 'transparent') when not selected so the .qe-row-bg hover
55
- // tint isn't shadowed by an inline background of the same property.
56
- ...(row.kind === 'track' && selected ? {background: 'rgba(60, 105, 209, 0.3)'} : {}),
57
- };
58
-
59
- if (row.kind === 'group') {
60
- return (
61
- <>
62
- <div className="qe-hover-bg" style={{...labelStyle, borderTop: `1px solid ${theme.border}`}} onClick={onToggleCollapse}>
63
- {!isRoot && (
64
- <button
65
- className="qe-hover"
66
- title={groupSelected ? 'Remove from group selection' : 'Select for grouping'}
67
- style={{...checkboxButton, color: groupSelected ? theme.accent : theme.textDim}}
68
- onClick={(e) => {
69
- e.stopPropagation();
70
- onToggleGroupSelect();
71
- }}
72
- >
73
- {groupSelected ? '☑' : '☐'}
74
- </button>
75
- )}
76
- <span style={{color: theme.textDim, fontSize: 10, width: 10}}>{collapsed ? '▸' : '▾'}</span>
77
- <span style={{flex: 1, overflow: 'hidden', textOverflow: 'ellipsis'}}>{row.name}</span>
78
- <button
79
- className="qe-hover"
80
- title="Add system to this group"
81
- style={{...iconButton, color: theme.accent, fontSize: 13}}
82
- onClick={(e) => {
83
- e.stopPropagation();
84
- let created: ParticleSystem | undefined;
85
- binding.apply(() => {
86
- created = createChildSystem(binding.system, {
87
- name: `system ${binding.subSystems.length + 1}`,
88
- parentNode: row.node,
89
- });
90
- binding.system._renderer?.addSystem(created);
91
- });
92
- if (created) {
93
- binding.addSubSystem(created);
94
- onSelect(created);
95
- }
96
- }}
97
- >
98
- +
99
- </button>
100
- {!isRoot && (
101
- <button className="qe-hover" title="Rename" style={{...iconButton, color: theme.textDim}} onClick={(e) => {
102
- e.stopPropagation();
103
- setRenaming(true);
104
- }}>
105
- ✎
106
- </button>
107
- )}
108
- {!isRoot && (
109
- <button
110
- className="qe-hover"
111
- title="Ungroup (moves its contents up a level)"
112
- style={{...iconButton, color: theme.textDim, fontSize: 12}}
113
- onClick={(e) => {
114
- e.stopPropagation();
115
- binding.apply(() => ungroupNode(row.node));
116
- }}
117
- >
118
- ⛶
119
- </button>
120
- )}
121
- {!isRoot && (
122
- <button
123
- className="qe-hover"
124
- title="Remove group"
125
- style={{...iconButton, color: '#e08c8c', fontSize: 12}}
126
- onClick={(e) => {
127
- e.stopPropagation();
128
- removeSystems(binding, collectSystems(buildEffectTree(row.node)), onSelect);
129
- binding.apply(() => row.node.dispose(true, false));
130
- }}
131
- >
132
- ✕
133
- </button>
134
- )}
135
- </div>
136
- <div className="qe-hover-bg" style={{height: ROW_HEIGHT, borderTop: `1px solid ${theme.border}`}} onClick={onToggleCollapse} />
137
- {renaming && (
138
- <PromptDialog
139
- title="Rename"
140
- defaultValue={row.name}
141
- onSubmit={(name) => {
142
- binding.apply(() => (row.node.name = name));
143
- setRenaming(false);
144
- }}
145
- onCancel={() => setRenaming(false)}
146
- />
147
- )}
148
- </>
149
- );
150
- }
151
-
152
- const {system} = row;
153
- const barLeft = offsetPx + row.startOffset * pxPerSecond;
154
- const barWidth = Math.max(2, row.duration * pxPerSecond);
155
-
156
- return (
157
- <>
158
- <div className="qe-hover-bg" style={{...labelStyle, opacity: hidden ? 0.5 : 1}} onClick={() => onSelect(system)}>
159
- {!isRoot && (
160
- <button
161
- className="qe-hover"
162
- title={groupSelected ? 'Remove from group selection' : 'Select for grouping'}
163
- style={{...checkboxButton, color: groupSelected ? theme.accent : theme.textDim}}
164
- onClick={(e) => {
165
- e.stopPropagation();
166
- onToggleGroupSelect();
167
- }}
168
- >
169
- {groupSelected ? '☑' : '☐'}
170
- </button>
171
- )}
172
- <button
173
- className="qe-hover"
174
- title={hidden ? 'Show in viewport' : 'Hide in viewport'}
175
- style={{...iconButton, color: hidden ? theme.textDim : theme.accent, fontSize: 12}}
176
- onClick={(e) => {
177
- e.stopPropagation();
178
- onToggleVisible?.();
179
- }}
180
- >
181
- {hidden ? '◯' : '◉'}
182
- </button>
183
- <span style={{color: theme.accent, fontSize: 10, width: 10}}>◆</span>
184
- <span style={{flex: 1, overflow: 'hidden', textOverflow: 'ellipsis'}}>
185
- {row.name}
186
- {system.onlyUsedByOther ? ' (sub)' : ''}
187
- </span>
188
- {!isRoot && (
189
- <button className="qe-hover" title="Rename" style={{...iconButton, color: theme.textDim}} onClick={(e) => {
190
- e.stopPropagation();
191
- setRenaming(true);
192
- }}>
193
- ✎
194
- </button>
195
- )}
196
- {!isRoot && row.removable && (
197
- <button
198
- className="qe-hover"
199
- title="Remove system"
200
- style={{...iconButton, color: '#e08c8c', fontSize: 12}}
201
- onClick={(e) => {
202
- e.stopPropagation();
203
- removeSystems(binding, [system], onSelect);
204
- }}
205
- >
206
- ✕
207
- </button>
208
- )}
209
- {selected && (
210
- <button
211
- className="qe-hover"
212
- title="Add child system"
213
- style={{...iconButton, color: theme.accent, fontSize: 13}}
214
- onClick={(e) => {
215
- e.stopPropagation();
216
- let created: ParticleSystem | undefined;
217
- binding.apply(() => {
218
- created = createChildSystem(system, {name: `system ${binding.subSystems.length + 1}`});
219
- binding.system._renderer?.addSystem(created);
220
- });
221
- if (created) {
222
- binding.addSubSystem(created);
223
- onSelect(created);
224
- }
225
- }}
226
- >
227
- +
228
- </button>
229
- )}
230
- </div>
231
- <div className="qe-hover-bg" style={{position: 'relative', height: ROW_HEIGHT}} onClick={() => onSelect(system)}>
232
- <div
233
- className="qe-hover"
234
- title={row.startOffsetVariable ? 'Start time varies per spawn' : undefined}
235
- style={{
236
- position: 'absolute',
237
- left: barLeft,
238
- width: barWidth,
239
- top: 5,
240
- height: ROW_HEIGHT - 10,
241
- borderRadius: 4,
242
- cursor: 'pointer',
243
- background: system.onlyUsedByOther ? 'repeating-linear-gradient(135deg, rgba(158,185,255,0.25), rgba(158,185,255,0.25) 4px, rgba(158,185,255,0.4) 4px, rgba(158,185,255,0.4) 8px)' : selected ? theme.accent : '#3c5aa0',
244
- border: system.onlyUsedByOther ? `1px dashed ${theme.accent}` : `1px solid ${theme.border}`,
245
- opacity: hidden ? 0.3 : system.onlyUsedByOther ? 0.75 : 1,
246
- boxSizing: 'border-box',
247
- }}
248
- >
249
- {row.looping && (
250
- <span style={{position: 'absolute', right: 4, top: -1, fontSize: 10, color: theme.text}}>⟳</span>
251
- )}
252
- </div>
253
- </div>
254
- {renaming && (
255
- <PromptDialog
256
- title="Rename"
257
- defaultValue={row.name}
258
- onSubmit={(name) => {
259
- binding.apply(() => (row.node.name = name));
260
- setRenaming(false);
261
- }}
262
- onCancel={() => setRenaming(false)}
263
- />
264
- )}
265
- </>
266
- );
267
- }
1
+ import React, {useState} from 'react';
2
+ import type {ParticleSystem} from 'babylon.quarks';
3
+ import type {TransformNode} from '@babylonjs/core/Meshes/transformNode';
4
+ import type {EffectBinding} from '../core/binding';
5
+ import {buildEffectTree, collectSystems} from '../core/effectTree';
6
+ import {createChildSystem, removeSystems} from '../core/systems';
7
+ import {ungroupNode} from '../core/groups';
8
+ import type {TimelineRow} from '../core/timeline';
9
+ import {PromptDialog} from './PromptDialog';
10
+ import {theme} from './theme';
11
+
12
+ const iconButton: React.CSSProperties = {background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, padding: 0};
13
+
14
+ const ROW_HEIGHT = 28;
15
+
16
+ export interface TimelineTrackRowProps {
17
+ row: TimelineRow;
18
+ binding: EffectBinding;
19
+ selected: boolean;
20
+ onSelectNode: (node: TransformNode | null) => void;
21
+ pxPerSecond: number;
22
+ /** Matches the ruler's left gutter so bars line up under the correct tick. */
23
+ offsetPx: number;
24
+ collapsed: boolean;
25
+ onToggleCollapse?: () => void;
26
+ /** Preview-only visibility (toggles `system.emitter.visible`); lets you isolate one track. */
27
+ hidden?: boolean;
28
+ onToggleVisible?: () => void;
29
+ /** Membership in the "group selected rows together" set (unrelated to inspector selection). */
30
+ groupSelected: boolean;
31
+ onToggleGroupSelect: () => void;
32
+ }
33
+
34
+ const checkboxButton: React.CSSProperties = {...iconButton, fontSize: 12, width: 12};
35
+
36
+ /** One row of the timeline: a label cell (name + actions) and, for tracks, a bar cell. */
37
+ export function TimelineTrackRow(props: TimelineTrackRowProps): React.ReactElement {
38
+ const {row, binding, selected, onSelectNode, pxPerSecond, offsetPx, collapsed, onToggleCollapse, hidden, onToggleVisible, groupSelected, onToggleGroupSelect} = props;
39
+ const [renaming, setRenaming] = useState(false);
40
+
41
+ const isRoot = row.node === binding.root;
42
+
43
+ const labelStyle: React.CSSProperties = {
44
+ display: 'flex',
45
+ alignItems: 'center',
46
+ gap: 6,
47
+ height: ROW_HEIGHT,
48
+ padding: '0 8px',
49
+ paddingLeft: 8 + row.depth * 14,
50
+ borderRadius: 6,
51
+ cursor: row.kind === 'track' ? 'pointer' : onToggleCollapse ? 'pointer' : 'default',
52
+ fontSize: 12.5,
53
+ color: selected ? theme.text : theme.textDim,
54
+ whiteSpace: 'nowrap',
55
+ boxSizing: 'border-box',
56
+ // Left unset (rather than 'transparent') when not selected so the .qe-row-bg hover
57
+ // tint isn't shadowed by an inline background of the same property.
58
+ ...(selected ? {background: 'rgba(60, 105, 209, 0.3)'} : {}),
59
+ };
60
+
61
+ if (row.kind === 'group') {
62
+ return (
63
+ <>
64
+ <div
65
+ className="qe-hover-bg"
66
+ style={{...labelStyle, borderTop: `1px solid ${theme.border}`}}
67
+ onClick={() => onSelectNode(row.node)}
68
+ >
69
+ {!isRoot && (
70
+ <button
71
+ className="qe-hover"
72
+ title={groupSelected ? 'Remove from group selection' : 'Select for grouping'}
73
+ style={{...checkboxButton, color: groupSelected ? theme.accent : theme.textDim}}
74
+ onClick={(e) => {
75
+ e.stopPropagation();
76
+ onToggleGroupSelect();
77
+ }}
78
+ >
79
+ {groupSelected ? '☑' : '☐'}
80
+ </button>
81
+ )}
82
+ <span
83
+ className="qe-hover"
84
+ title={collapsed ? 'Expand' : 'Collapse'}
85
+ style={{color: theme.textDim, fontSize: 10, width: 10, cursor: 'pointer'}}
86
+ onClick={(e) => {
87
+ e.stopPropagation();
88
+ onToggleCollapse?.();
89
+ }}
90
+ >
91
+ {collapsed ? '▸' : '▾'}
92
+ </span>
93
+ <span style={{flex: 1, overflow: 'hidden', textOverflow: 'ellipsis'}}>{row.name}</span>
94
+ <button
95
+ className="qe-hover"
96
+ title="Add system to this group"
97
+ style={{...iconButton, color: theme.accent, fontSize: 13}}
98
+ onClick={(e) => {
99
+ e.stopPropagation();
100
+ let created: ParticleSystem | undefined;
101
+ binding.apply(() => {
102
+ created = createChildSystem(binding.system, {
103
+ name: `system ${binding.subSystems.length + 1}`,
104
+ parentNode: row.node,
105
+ });
106
+ binding.system._renderer?.addSystem(created);
107
+ });
108
+ if (created) {
109
+ binding.addSubSystem(created);
110
+ onSelectNode(created.emitter);
111
+ }
112
+ }}
113
+ >
114
+ +
115
+ </button>
116
+ {!isRoot && (
117
+ <button className="qe-hover" title="Rename" style={{...iconButton, color: theme.textDim}} onClick={(e) => {
118
+ e.stopPropagation();
119
+ setRenaming(true);
120
+ }}>
121
+ ✎
122
+ </button>
123
+ )}
124
+ {!isRoot && (
125
+ <button
126
+ className="qe-hover"
127
+ title="Ungroup (moves its contents up a level)"
128
+ style={{...iconButton, color: theme.textDim, fontSize: 12}}
129
+ onClick={(e) => {
130
+ e.stopPropagation();
131
+ binding.apply(() => ungroupNode(row.node));
132
+ }}
133
+ >
134
+ ⛶
135
+ </button>
136
+ )}
137
+ {!isRoot && (
138
+ <button
139
+ className="qe-hover"
140
+ title="Remove group"
141
+ style={{...iconButton, color: '#e08c8c', fontSize: 12}}
142
+ onClick={(e) => {
143
+ e.stopPropagation();
144
+ removeSystems(binding, collectSystems(buildEffectTree(row.node)), (s) => onSelectNode(s.emitter));
145
+ binding.apply(() => row.node.dispose(true, false));
146
+ }}
147
+ >
148
+ ✕
149
+ </button>
150
+ )}
151
+ </div>
152
+ <div className="qe-hover-bg" style={{height: ROW_HEIGHT, borderTop: `1px solid ${theme.border}`}} onClick={() => onSelectNode(row.node)} />
153
+ {renaming && (
154
+ <PromptDialog
155
+ title="Rename"
156
+ defaultValue={row.name}
157
+ onSubmit={(name) => {
158
+ binding.apply(() => (row.node.name = name));
159
+ setRenaming(false);
160
+ }}
161
+ onCancel={() => setRenaming(false)}
162
+ />
163
+ )}
164
+ </>
165
+ );
166
+ }
167
+
168
+ const {system} = row;
169
+ const barLeft = offsetPx + row.startOffset * pxPerSecond;
170
+ const barWidth = Math.max(2, row.duration * pxPerSecond);
171
+
172
+ return (
173
+ <>
174
+ <div
175
+ className="qe-hover-bg"
176
+ style={{...labelStyle, opacity: hidden ? 0.5 : 1}}
177
+ onClick={() => onSelectNode(row.node)}
178
+ >
179
+ {!isRoot && (
180
+ <button
181
+ className="qe-hover"
182
+ title={groupSelected ? 'Remove from group selection' : 'Select for grouping'}
183
+ style={{...checkboxButton, color: groupSelected ? theme.accent : theme.textDim}}
184
+ onClick={(e) => {
185
+ e.stopPropagation();
186
+ onToggleGroupSelect();
187
+ }}
188
+ >
189
+ {groupSelected ? '☑' : '☐'}
190
+ </button>
191
+ )}
192
+ <button
193
+ className="qe-hover"
194
+ title={hidden ? 'Show in viewport' : 'Hide in viewport'}
195
+ style={{...iconButton, color: hidden ? theme.textDim : theme.accent, fontSize: 12}}
196
+ onClick={(e) => {
197
+ e.stopPropagation();
198
+ onToggleVisible?.();
199
+ }}
200
+ >
201
+ {hidden ? '◯' : '◉'}
202
+ </button>
203
+ <span style={{color: theme.accent, fontSize: 10, width: 10}}>◆</span>
204
+ <span style={{flex: 1, overflow: 'hidden', textOverflow: 'ellipsis'}}>
205
+ {row.name}
206
+ {system.onlyUsedByOther ? ' (sub)' : ''}
207
+ </span>
208
+ {!isRoot && (
209
+ <button className="qe-hover" title="Rename" style={{...iconButton, color: theme.textDim}} onClick={(e) => {
210
+ e.stopPropagation();
211
+ setRenaming(true);
212
+ }}>
213
+ ✎
214
+ </button>
215
+ )}
216
+ {!isRoot && row.removable && (
217
+ <button
218
+ className="qe-hover"
219
+ title="Remove system"
220
+ style={{...iconButton, color: '#e08c8c', fontSize: 12}}
221
+ onClick={(e) => {
222
+ e.stopPropagation();
223
+ removeSystems(binding, [system], (s) => onSelectNode(s.emitter));
224
+ }}
225
+ >
226
+ ✕
227
+ </button>
228
+ )}
229
+ {selected && (
230
+ <button
231
+ className="qe-hover"
232
+ title="Add child system"
233
+ style={{...iconButton, color: theme.accent, fontSize: 13}}
234
+ onClick={(e) => {
235
+ e.stopPropagation();
236
+ let created: ParticleSystem | undefined;
237
+ binding.apply(() => {
238
+ created = createChildSystem(system, {name: `system ${binding.subSystems.length + 1}`});
239
+ binding.system._renderer?.addSystem(created);
240
+ });
241
+ if (created) {
242
+ binding.addSubSystem(created);
243
+ onSelectNode(created.emitter);
244
+ }
245
+ }}
246
+ >
247
+ +
248
+ </button>
249
+ )}
250
+ </div>
251
+ <div
252
+ className="qe-hover-bg"
253
+ style={{position: 'relative', height: ROW_HEIGHT}}
254
+ onClick={() => onSelectNode(row.node)}
255
+ >
256
+ <div
257
+ className="qe-hover"
258
+ title={row.startOffsetVariable ? 'Start time varies per spawn' : undefined}
259
+ style={{
260
+ position: 'absolute',
261
+ left: barLeft,
262
+ width: barWidth,
263
+ top: 5,
264
+ height: ROW_HEIGHT - 10,
265
+ borderRadius: 4,
266
+ cursor: 'pointer',
267
+ background: system.onlyUsedByOther ? 'repeating-linear-gradient(135deg, rgba(158,185,255,0.25), rgba(158,185,255,0.25) 4px, rgba(158,185,255,0.4) 4px, rgba(158,185,255,0.4) 8px)' : selected ? theme.accent : '#3c5aa0',
268
+ border: system.onlyUsedByOther ? `1px dashed ${theme.accent}` : `1px solid ${theme.border}`,
269
+ opacity: hidden ? 0.3 : system.onlyUsedByOther ? 0.75 : 1,
270
+ boxSizing: 'border-box',
271
+ }}
272
+ >
273
+ {row.looping && (
274
+ <span style={{position: 'absolute', right: 4, top: -1, fontSize: 10, color: theme.text}}>⟳</span>
275
+ )}
276
+ </div>
277
+ </div>
278
+ {renaming && (
279
+ <PromptDialog
280
+ title="Rename"
281
+ defaultValue={row.name}
282
+ onSubmit={(name) => {
283
+ binding.apply(() => (row.node.name = name));
284
+ setRenaming(false);
285
+ }}
286
+ onCancel={() => setRenaming(false)}
287
+ />
288
+ )}
289
+ </>
290
+ );
291
+ }