@jupyterlab/settingeditor 4.0.0-alpha.8 → 4.0.0-beta.0

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.
@@ -0,0 +1,218 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import { ISettingRegistry, Settings } from '@jupyterlab/settingregistry';
7
+ import { ITranslator } from '@jupyterlab/translation';
8
+ import { IFormRendererRegistry } from '@jupyterlab/ui-components';
9
+ import { ISignal } from '@lumino/signaling';
10
+ import type { Field } from '@rjsf/utils';
11
+ import React, { useEffect, useState } from 'react';
12
+ import { PluginList } from './pluginlist';
13
+ import { SettingsFormEditor } from './SettingsFormEditor';
14
+
15
+ export interface ISettingsPanelProps {
16
+ /**
17
+ * List of Settings objects that provide schema and values
18
+ * of plugins.
19
+ */
20
+ settings: Settings[];
21
+
22
+ /**
23
+ * Form component registry that provides renderers
24
+ * for the form editor.
25
+ */
26
+ editorRegistry: IFormRendererRegistry;
27
+
28
+ /**
29
+ * Handler for when selection change is triggered by scrolling
30
+ * in the SettingsPanel.
31
+ */
32
+ onSelect: (id: string) => void;
33
+
34
+ /**
35
+ * Signal that fires when a selection is made in the plugin list.
36
+ */
37
+ handleSelectSignal: ISignal<PluginList, string>;
38
+
39
+ /**
40
+ * Translator object
41
+ */
42
+ translator: ITranslator;
43
+
44
+ /**
45
+ * Callback to update the plugin list to display plugins with
46
+ * invalid / unsaved settings in red.
47
+ */
48
+ hasError: (id: string, error: boolean) => void;
49
+
50
+ /**
51
+ * Sends the updated dirty state to the parent class.
52
+ */
53
+ updateDirtyState: (dirty: boolean) => void;
54
+
55
+ /**
56
+ * Signal that sends updated filter when search value changes.
57
+ */
58
+ updateFilterSignal: ISignal<
59
+ PluginList,
60
+ (plugin: ISettingRegistry.IPlugin) => string[] | null
61
+ >;
62
+
63
+ /**
64
+ * If the settings editor is created with an initial search query, an initial
65
+ * filter function is passed to the settings panel.
66
+ */
67
+ initialFilter: (item: ISettingRegistry.IPlugin) => string[] | null;
68
+ }
69
+
70
+ /**
71
+ * React component that displays a list of SettingsFormEditor
72
+ * components.
73
+ */
74
+ export const SettingsPanel: React.FC<ISettingsPanelProps> = ({
75
+ settings,
76
+ editorRegistry,
77
+ onSelect,
78
+ handleSelectSignal,
79
+ hasError,
80
+ updateDirtyState,
81
+ updateFilterSignal,
82
+ translator,
83
+ initialFilter
84
+ }: ISettingsPanelProps): JSX.Element => {
85
+ const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null);
86
+ const [filterPlugin, setFilter] = useState<
87
+ (plugin: ISettingRegistry.IPlugin) => string[] | null
88
+ >(() => initialFilter);
89
+
90
+ // Refs used to keep track of "selected" plugin based on scroll location
91
+ const editorRefs: {
92
+ [pluginId: string]: React.RefObject<HTMLDivElement>;
93
+ } = {};
94
+ for (const setting of settings) {
95
+ editorRefs[setting.id] = React.useRef(null);
96
+ }
97
+ const wrapperRef: React.RefObject<HTMLDivElement> = React.useRef(null);
98
+ const editorDirtyStates: React.RefObject<{
99
+ [id: string]: boolean;
100
+ }> = React.useRef({});
101
+
102
+ useEffect(() => {
103
+ const onFilterUpdate = (
104
+ list: PluginList,
105
+ newFilter: (plugin: ISettingRegistry.IPlugin) => string[] | null
106
+ ) => {
107
+ setFilter(() => newFilter);
108
+ for (const pluginSettings of settings) {
109
+ const filtered = newFilter(pluginSettings.plugin);
110
+ if (filtered === null || filtered.length > 0) {
111
+ setExpandedPlugin(pluginSettings.id);
112
+ break;
113
+ }
114
+ }
115
+ };
116
+
117
+ // Set first visible plugin as expanded plugin on initial load.
118
+ for (const pluginSettings of settings) {
119
+ const filtered = filterPlugin(pluginSettings.plugin);
120
+ if (filtered === null || filtered.length > 0) {
121
+ setExpandedPlugin(pluginSettings.id);
122
+ break;
123
+ }
124
+ }
125
+
126
+ // When filter updates, only show plugins that match search.
127
+ updateFilterSignal.connect(onFilterUpdate);
128
+
129
+ const onSelectChange = (list: PluginList, pluginId: string) => {
130
+ setExpandedPlugin(expandedPlugin !== pluginId ? pluginId : null);
131
+ // Scroll to the plugin when a selection is made in the left panel.
132
+ editorRefs[pluginId]?.current?.scrollIntoView(true);
133
+ };
134
+ handleSelectSignal?.connect?.(onSelectChange);
135
+
136
+ return () => {
137
+ updateFilterSignal.disconnect(onFilterUpdate);
138
+ handleSelectSignal?.disconnect?.(onSelectChange);
139
+ };
140
+ }, []);
141
+
142
+ const updateDirtyStates = React.useCallback(
143
+ (id: string, dirty: boolean) => {
144
+ if (editorDirtyStates.current) {
145
+ editorDirtyStates.current[id] = dirty;
146
+ for (const editor in editorDirtyStates.current) {
147
+ if (editorDirtyStates.current[editor]) {
148
+ updateDirtyState(true);
149
+ return;
150
+ }
151
+ }
152
+ }
153
+ updateDirtyState(false);
154
+ },
155
+ [editorDirtyStates, updateDirtyState]
156
+ );
157
+
158
+ const renderers = React.useMemo(
159
+ () =>
160
+ Object.entries(editorRegistry.renderers).reduce<{
161
+ [plugin: string]: { [property: string]: Field };
162
+ }>((agg, [id, renderer]) => {
163
+ const splitPosition = id.lastIndexOf('.');
164
+ const pluginId = id.substring(0, splitPosition);
165
+ const propertyName = id.substring(splitPosition + 1);
166
+ if (!agg[pluginId]) {
167
+ agg[pluginId] = {};
168
+ }
169
+ if (!agg[pluginId][propertyName] && renderer.fieldRenderer) {
170
+ agg[pluginId][propertyName] = renderer.fieldRenderer;
171
+ }
172
+ return agg;
173
+ }, {}),
174
+ [editorRegistry]
175
+ );
176
+
177
+ return (
178
+ <div className="jp-SettingsPanel" ref={wrapperRef}>
179
+ {settings.map(pluginSettings => {
180
+ // Pass filtered results to SettingsFormEditor to only display filtered fields.
181
+ const filtered = filterPlugin(pluginSettings.plugin);
182
+ // If filtered results are an array, only show if the array is non-empty.
183
+ if (filtered !== null && filtered.length === 0) {
184
+ return undefined;
185
+ }
186
+ return (
187
+ <div
188
+ ref={editorRefs[pluginSettings.id]}
189
+ className="jp-SettingsForm"
190
+ key={`${pluginSettings.id}SettingsEditor`}
191
+ >
192
+ <SettingsFormEditor
193
+ isCollapsed={pluginSettings.id !== expandedPlugin}
194
+ onCollapseChange={(willCollapse: boolean) => {
195
+ if (!willCollapse) {
196
+ setExpandedPlugin(pluginSettings.id);
197
+ } else if (pluginSettings.id === expandedPlugin) {
198
+ setExpandedPlugin(null);
199
+ }
200
+ }}
201
+ filteredValues={filtered}
202
+ settings={pluginSettings}
203
+ renderers={renderers}
204
+ hasError={(error: boolean) => {
205
+ hasError(pluginSettings.id, error);
206
+ }}
207
+ updateDirtyState={(dirty: boolean) => {
208
+ updateDirtyStates(pluginSettings.id, dirty);
209
+ }}
210
+ onSelect={onSelect}
211
+ translator={translator}
212
+ />
213
+ </div>
214
+ );
215
+ })}
216
+ </div>
217
+ );
218
+ };
package/src/tokens.ts ADDED
@@ -0,0 +1,33 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { IWidgetTracker, MainAreaWidget } from '@jupyterlab/apputils';
5
+ import { Token } from '@lumino/coreutils';
6
+ import { JsonSettingEditor as JSONSettingEditor } from './jsonsettingeditor';
7
+ import { SettingsEditor } from './settingseditor';
8
+
9
+ /**
10
+ * The setting editor tracker token.
11
+ */
12
+ export const ISettingEditorTracker = new Token<ISettingEditorTracker>(
13
+ '@jupyterlab/settingeditor:ISettingEditorTracker'
14
+ );
15
+
16
+ /**
17
+ * The setting editor tracker token.
18
+ */
19
+ export const IJSONSettingEditorTracker = new Token<IJSONSettingEditorTracker>(
20
+ '@jupyterlab/settingeditor:IJSONSettingEditorTracker'
21
+ );
22
+
23
+ /**
24
+ * A class that tracks the setting editor.
25
+ */
26
+ export interface IJSONSettingEditorTracker
27
+ extends IWidgetTracker<MainAreaWidget<JSONSettingEditor>> {}
28
+
29
+ /**
30
+ * A class that tracks the setting editor.
31
+ */
32
+ export interface ISettingEditorTracker
33
+ extends IWidgetTracker<MainAreaWidget<SettingsEditor>> {}
package/style/base.css CHANGED
@@ -18,9 +18,9 @@
18
18
  min-width: 360px;
19
19
  min-height: 240px;
20
20
  background-color: var(--jp-layout-color0);
21
+ color: var(--jp-ui-font-color0);
21
22
  margin-top: -1px;
22
23
  outline: none;
23
- color: var(--jp-content-font-color1) !important;
24
24
 
25
25
  /* This is needed so that all font sizing of children done in ems is
26
26
  * relative to this base size */
@@ -90,16 +90,13 @@
90
90
  right: 0;
91
91
  }
92
92
 
93
- .jp-PluginList .jp-SettingsHeader {
94
- display: flex;
95
- flex-basis: 100%;
96
- }
97
-
98
- .jp-PluginList .jp-SettingsHeader button {
99
- color: var(--jp-private-notebook-selected-color);
100
- white-space: nowrap;
93
+ .jp-PluginList .jp-PluginList-header {
94
+ border-bottom: var(--jp-border-width) solid var(--jp-border-color2);
95
+ border-top: var(--jp-border-width) solid var(--jp-border-color2);
96
+ color: var(--jp-ui-font-color1);
101
97
  }
102
98
 
99
+ .jp-PluginList .jp-PluginList-noResults,
103
100
  .jp-PluginList .jp-PluginList-header {
104
101
  flex: 0 0 auto;
105
102
  font-weight: 600;
@@ -110,13 +107,18 @@
110
107
  margin: 10px;
111
108
  border-bottom: var(--jp-border-width) solid var(--jp-border-color2);
112
109
  border-top: var(--jp-border-width) solid var(--jp-border-color2);
113
- color: var(--jp-content-font-color1);
110
+ color: var(--jp-ui-font-color1);
114
111
  }
115
112
 
116
113
  .jp-PluginList .jp-SelectedIndicator {
117
114
  width: 3px;
118
115
  background-color: var(--jp-brand-color1);
119
116
  height: var(--jp-cell-collapser-min-height);
117
+ visibility: hidden;
118
+ }
119
+
120
+ .jp-PluginList .jp-mod-selected .jp-SelectedIndicator {
121
+ visibility: inherit;
120
122
  }
121
123
 
122
124
  .jp-PluginList .jp-ErrorPlugin .jp-SelectedIndicator {
@@ -127,20 +129,11 @@
127
129
  color: var(--jp-error-color0);
128
130
  }
129
131
 
130
- .jp-PluginList button:not(.jp-mod-selected) {
131
- margin-left: 3px;
132
- }
133
-
134
132
  .jp-PluginList button.jp-mod-selected span {
135
133
  font-weight: var(--jp-content-heading-font-weight);
136
134
  color: var(--jp-brand-color1);
137
135
  }
138
136
 
139
- .jp-PluginList button span {
140
- color: var(--jp-content-font-color1);
141
- line-height: var(--jp-cell-collapser-min-height);
142
- }
143
-
144
137
  .jp-FormComponent li span {
145
138
  overflow: hidden;
146
139
  }
@@ -157,10 +150,6 @@
157
150
  background-color: var(--jp-layout-color0);
158
151
  }
159
152
 
160
- .jp-PluginList-Searcher {
161
- margin: 5px;
162
- }
163
-
164
153
  ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
165
154
  background-image: var(--jp-icon-file-selected);
166
155
  }
@@ -217,46 +206,17 @@ ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
217
206
  text-align: right;
218
207
  }
219
208
 
220
- .jp-SettingsPanel fieldset input,
221
- .jp-SettingsPanel fieldset select,
222
- .jp-SettingsPanel fieldset textarea {
223
- font-size: var(--jp-content-font-size2);
224
- border-color: var(--jp-input-border-color);
225
- border-style: solid;
226
- border-radius: 5px;
227
- border-width: 1px;
228
- padding: 6px 8px;
229
- background: none;
230
- color: var(--jp-content-font-color0);
231
- height: inherit;
232
- }
233
-
234
- .jp-SettingsPanel fieldset input[type='checkbox'] {
235
- position: relative;
236
- top: 2px;
237
- margin-left: 0;
238
- }
239
-
240
- /** copy of `input.jp-mod-styled:focus` style */
241
- .jp-SettingsPanel fieldset input:focus {
242
- border: var(--jp-border-width) solid var(--md-blue-500);
243
- box-shadow: inset 0 0 4px var(--md-blue-300);
244
- }
245
-
246
- .jp-SettingsPanel .checkbox label {
247
- cursor: pointer;
209
+ .jp-SettingsRawEditor .cm-editor {
210
+ height: 100%;
248
211
  }
249
212
 
250
- .jp-SettingsPanel .checkbox .field-description {
251
- /* Disable default description field for checkbox:
252
- because other widgets do not have description fields,
253
- we add descriptions to each widget on the field level.
254
- */
255
- display: none;
213
+ .jp-SettingsPanel .checkbox p {
214
+ font-size: var(--jp-content-font-size1);
256
215
  }
257
216
 
258
- .jp-SettingsPanel button[type='submit'] {
259
- display: none;
217
+ .jp-SettingsPanel .checkbox {
218
+ display: flex;
219
+ flex-direction: column-reverse;
260
220
  }
261
221
 
262
222
  .jp-SettingsPanel .form-group {
@@ -265,43 +225,6 @@ ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
265
225
  margin-top: 5px;
266
226
  }
267
227
 
268
- .jp-SettingsPanel .jp-objectFieldWrapper .form-group {
269
- padding: 2px 8px 2px var(--jp-private-settingeditor-modifier-indent);
270
- margin-top: 2px;
271
- }
272
-
273
- .jp-ArrayOperations {
274
- margin-left: 8px;
275
- }
276
-
277
- .jp-SettingsPanel .jp-FormGroup-content {
278
- display: flex;
279
- align-items: center;
280
- flex-wrap: wrap;
281
- }
282
-
283
- .jp-SettingsPanel .jp-FormGroup-contentItem {
284
- margin-left: 7px;
285
- }
286
-
287
- .jp-SettingsPanel .jp-FormGroup-description {
288
- flex-basis: 100%;
289
- padding: 4px 7px;
290
- }
291
-
292
- .jp-SettingsPanel #root__description {
293
- display: none;
294
- }
295
-
296
- .jp-SettingsPanel fieldset {
297
- border: none;
298
- padding: 0;
299
- }
300
-
301
- .jp-SettingsPanel fieldset:not(:first-child) {
302
- margin-left: 7px;
303
- }
304
-
305
228
  .jp-SettingsPanel .jp-SaveSettingsBanner {
306
229
  position: absolute;
307
230
  bottom: 0;
@@ -323,71 +246,6 @@ ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
323
246
  color: var(--jp-brand-color0);
324
247
  }
325
248
 
326
- .jp-SettingsPanel .form-group.small-field:hover {
327
- background: var(--jp-border-color3);
328
- }
329
-
330
- .jp-SettingsPanel button.jp-mod-styled {
331
- cursor: pointer;
332
- }
333
-
334
- .jp-SettingsPanel button.jp-mod-styled:disabled {
335
- cursor: not-allowed;
336
- opacity: 0.5;
337
- }
338
-
339
- .jp-SettingsPanel .array-item button {
340
- margin: 2px;
341
- }
342
-
343
- .jp-openJSONSettingsEditor {
344
- position: absolute;
345
- bottom: 0;
346
- left: 0;
347
- }
348
-
349
- .jp-openJSONSettingsEditor button {
350
- border: 1px solid var(--jp-border-color1);
351
- color: var(--jp-ui-font-color0);
352
- padding: 5px;
353
- margin: 5px;
354
- cursor: pointer;
355
- background-color: var(--jp-border-color2);
356
- display: flex;
357
- align-items: center;
358
- }
359
-
360
- .jp-openJSONSettingsEditor button > div {
361
- display: flex;
362
- }
363
-
364
- .jp-openJSONSettingsEditor svg#icon {
365
- height: 1.5em;
366
- }
367
-
368
- .jp-SettingsPanel .array-item {
369
- border: 1px solid var(--jp-border-color2);
370
- border-radius: 4px;
371
- margin: 4px;
372
- }
373
-
374
- .jp-SettingsPanel .field-array-of-string .array-item {
375
- /* Display `jp-ArrayOperations` buttons side-by-side with content except
376
- for small screens where flex-wrap will place them one below the other.
377
- */
378
- display: flex;
379
- align-items: center;
380
- flex-wrap: wrap;
381
- }
382
-
383
- .jp-SettingsPanel .jp-root > fieldset > legend {
384
- display: none;
385
- }
386
-
387
- .jp-SettingsPanel .jp-root > fieldset > p {
388
- display: none;
389
- }
390
-
391
249
  .jp-SettingsPanel .jp-SettingsHeader h2 {
392
250
  font-size: var(--jp-content-font-size3);
393
251
  color: var(--jp-ui-font-color0);
@@ -403,20 +261,6 @@ ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
403
261
  line-height: var(--jp-content-font-size3);
404
262
  }
405
263
 
406
- .jp-SettingsPanel legend {
407
- font-size: var(--jp-content-font-size2);
408
- color: var(--jp-ui-font-color0);
409
- flex-basis: 100%;
410
- padding: 4px 0;
411
- font-weight: var(--jp-content-header-font-weight);
412
- border-bottom: 1px solid var(--jp-border-color2);
413
- }
414
-
415
- .jp-SettingsPanel .field-description {
416
- padding: 4px 0;
417
- white-space: pre-wrap;
418
- }
419
-
420
264
  .jp-SettingsPanel .jp-SettingsTitle {
421
265
  display: flex;
422
266
  align-items: center;
@@ -444,47 +288,49 @@ ul.jp-PluginList li.jp-mod-selected span.jp-PluginList-icon.jp-FileIcon {
444
288
  margin: 8px 12px 0;
445
289
  }
446
290
 
291
+ .jp-PluginList mark {
292
+ background-color: transparent;
293
+ font-weight: bold;
294
+ color: var(--jp-ui-font-color1);
295
+ }
296
+
447
297
  .jp-PluginList-entry {
448
298
  display: flex;
449
- flex-direction: row;
299
+ flex-direction: column;
450
300
  border: 1px solid transparent;
451
301
  background: transparent;
452
302
  overflow: hidden;
453
303
  padding: 4px 0 4px 4px;
454
- text-overflow: ellipsis;
455
304
  white-space: nowrap;
456
- width: -webkit-fill-available;
457
- }
458
-
459
- .jp-SettingsPanel .jp-SettingsHeader-Name {
460
- text-transform: inherit;
461
- font-size: var(--jp-content-font-size3);
462
305
  }
463
306
 
464
- .jp-SettingsPanel .jp-modifiedIndicator {
465
- width: 5px;
466
- background-color: var(--jp-brand-color2);
467
- margin-top: 0;
468
- margin-left: calc(var(--jp-private-settingeditor-modifier-indent) * -1);
469
- flex-shrink: 0;
307
+ .jp-PluginList-entry:hover {
308
+ background: var(--jp-layout-color1);
470
309
  }
471
310
 
472
- .jp-SettingsPanel .jp-FormGroup-fieldLabel {
473
- font-size: var(--jp-content-font-size1);
474
- font-weight: normal;
475
- min-width: 120px;
311
+ .jp-PluginList-entry li {
312
+ margin-left: 27px;
313
+ margin-top: 5px;
314
+ color: var(--jp-ui-font-color1);
315
+ overflow-x: hidden;
316
+ text-overflow: ellipsis;
476
317
  }
477
318
 
478
- .jp-SettingsPanel .jp-modifiedIndicator.jp-errorIndicator {
479
- background-color: var(--jp-error-color0);
319
+ .jp-PluginList-entry-label {
320
+ display: flex;
480
321
  }
481
322
 
482
- .jp-SettingsPanel .validationErrors {
483
- color: var(--jp-error-color0);
323
+ .jp-PluginList-entry-label-text {
324
+ text-overflow: ellipsis;
325
+ overflow-x: hidden;
326
+ white-space: nowrap;
327
+ color: var(--jp-ui-font-color1);
328
+ line-height: var(--jp-cell-collapser-min-height);
484
329
  }
485
330
 
486
- .jp-SettingsPanel .panel.errors {
487
- display: none;
331
+ .jp-SettingsPanel .jp-SettingsHeader-Name {
332
+ text-transform: inherit;
333
+ font-size: var(--jp-content-font-size3);
488
334
  }
489
335
 
490
336
  .jp-SettingsPanel .jp-SettingsEditor {
@@ -1,13 +0,0 @@
1
- import { ISignal } from '@lumino/signaling';
2
- import { SplitPanel as SPanel } from '@lumino/widgets';
3
- /**
4
- * A deprecated split panel that will be removed when the phosphor split panel
5
- * supports a handle moved signal. See https://github.com/phosphorjs/phosphor/issues/297.
6
- */
7
- export declare class SplitPanel extends SPanel {
8
- /**
9
- * Emits when the split handle has moved.
10
- */
11
- readonly handleMoved: ISignal<any, void>;
12
- handleEvent(event: Event): void;
13
- }
package/lib/splitpanel.js DELETED
@@ -1,26 +0,0 @@
1
- /* -----------------------------------------------------------------------------
2
- | Copyright (c) Jupyter Development Team.
3
- | Distributed under the terms of the Modified BSD License.
4
- |----------------------------------------------------------------------------*/
5
- import { Signal } from '@lumino/signaling';
6
- import { SplitPanel as SPanel } from '@lumino/widgets';
7
- /**
8
- * A deprecated split panel that will be removed when the phosphor split panel
9
- * supports a handle moved signal. See https://github.com/phosphorjs/phosphor/issues/297.
10
- */
11
- export class SplitPanel extends SPanel {
12
- constructor() {
13
- super(...arguments);
14
- /**
15
- * Emits when the split handle has moved.
16
- */
17
- this.handleMoved = new Signal(this);
18
- }
19
- handleEvent(event) {
20
- super.handleEvent(event);
21
- if (event.type === 'mouseup') {
22
- this.handleMoved.emit(undefined);
23
- }
24
- }
25
- }
26
- //# sourceMappingURL=splitpanel.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"splitpanel.js","sourceRoot":"","sources":["../src/splitpanel.ts"],"names":[],"mappings":"AAAA;;;+EAG+E;AAE/E,OAAO,EAAW,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;GAGG;AACH,MAAM,OAAO,UAAW,SAAQ,MAAM;IAAtC;;QACE;;WAEG;QACM,gBAAW,GAAuB,IAAI,MAAM,CAAY,IAAI,CAAC,CAAC;IASzE,CAAC;IAPC,WAAW,CAAC,KAAY;QACtB,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAEzB,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE;YAC3B,IAAI,CAAC,WAAiC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;SACzD;IACH,CAAC;CACF"}