foldkit 0.32.0 → 0.33.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.
- package/README.md +3 -3
- package/dist/devtools/overlay-styles.d.ts +3 -0
- package/dist/devtools/overlay-styles.d.ts.map +1 -0
- package/dist/devtools/overlay-styles.js +514 -0
- package/dist/devtools/overlay.d.ts +5 -0
- package/dist/devtools/overlay.d.ts.map +1 -0
- package/dist/devtools/overlay.js +632 -0
- package/dist/devtools/store.d.ts +36 -0
- package/dist/devtools/store.d.ts.map +1 -0
- package/dist/devtools/store.js +94 -0
- package/dist/html/index.d.ts +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/runtime/runtime.d.ts +26 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +48 -3
- package/dist/ui/combobox/multi.d.ts +2 -2
- package/dist/ui/combobox/shared.d.ts +2 -2
- package/dist/ui/combobox/single.d.ts +2 -2
- package/dist/ui/listbox/multi.d.ts +5 -5
- package/dist/ui/listbox/shared.d.ts +2 -2
- package/dist/ui/listbox/single.d.ts +5 -5
- package/package.json +5 -4
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { Array as Array_, Effect, HashSet, Match as M, Number as Number_, Option, Predicate, Record, Schema as S, Stream, String as String_, SubscriptionRef, pipe, } from 'effect';
|
|
3
|
+
import { OptionExt } from '../effectExtensions';
|
|
4
|
+
import { html } from '../html';
|
|
5
|
+
import { m } from '../message';
|
|
6
|
+
import { makeElement } from '../runtime/runtime';
|
|
7
|
+
import { makeSubscriptions } from '../runtime/subscription';
|
|
8
|
+
import { evo } from '../struct';
|
|
9
|
+
import * as Tabs from '../ui/tabs';
|
|
10
|
+
import { overlayStyles } from './overlay-styles';
|
|
11
|
+
import { INIT_INDEX } from './store';
|
|
12
|
+
// MODEL
|
|
13
|
+
const DisplayEntry = S.Struct({
|
|
14
|
+
tag: S.String,
|
|
15
|
+
commandCount: S.Number,
|
|
16
|
+
timestamp: S.Number,
|
|
17
|
+
isModelChanged: S.Boolean,
|
|
18
|
+
});
|
|
19
|
+
const INSPECTOR_TABS_ID = 'dt-inspector';
|
|
20
|
+
const InspectorTabsModel = S.Struct({
|
|
21
|
+
id: S.String,
|
|
22
|
+
activeIndex: S.Number,
|
|
23
|
+
focusedIndex: S.Number,
|
|
24
|
+
activationMode: S.Literal('Automatic', 'Manual'),
|
|
25
|
+
});
|
|
26
|
+
const Model = S.Struct({
|
|
27
|
+
isOpen: S.Boolean,
|
|
28
|
+
entries: S.Array(DisplayEntry),
|
|
29
|
+
startIndex: S.Number,
|
|
30
|
+
isPaused: S.Boolean,
|
|
31
|
+
pausedAtIndex: S.Number,
|
|
32
|
+
maybeSelectedIndex: S.OptionFromSelf(S.Number),
|
|
33
|
+
maybeInspectedModel: S.OptionFromSelf(S.Unknown),
|
|
34
|
+
maybeInspectedMessage: S.OptionFromSelf(S.Unknown),
|
|
35
|
+
expandedPaths: S.HashSetFromSelf(S.String),
|
|
36
|
+
changedPaths: S.HashSetFromSelf(S.String),
|
|
37
|
+
affectedPaths: S.HashSetFromSelf(S.String),
|
|
38
|
+
inspectorTabs: InspectorTabsModel,
|
|
39
|
+
});
|
|
40
|
+
// MESSAGE
|
|
41
|
+
const ClickedToggle = m('ClickedToggle');
|
|
42
|
+
const ClickedRow = m('ClickedRow', { index: S.Number });
|
|
43
|
+
const ClickedResume = m('ClickedResume');
|
|
44
|
+
const ClickedClear = m('ClickedClear');
|
|
45
|
+
const CompletedJump = m('CompletedJump');
|
|
46
|
+
const CompletedResume = m('CompletedResume');
|
|
47
|
+
const ClickedFollowLatest = m('ClickedFollowLatest');
|
|
48
|
+
const CompletedClear = m('CompletedClear');
|
|
49
|
+
const CompletedScroll = m('CompletedScroll');
|
|
50
|
+
const ReceivedInspectedState = m('ReceivedInspectedState', {
|
|
51
|
+
model: S.Unknown,
|
|
52
|
+
maybeMessage: S.OptionFromSelf(S.Unknown),
|
|
53
|
+
changedPaths: S.HashSetFromSelf(S.String),
|
|
54
|
+
affectedPaths: S.HashSetFromSelf(S.String),
|
|
55
|
+
});
|
|
56
|
+
const ToggledTreeNode = m('ToggledTreeNode', { path: S.String });
|
|
57
|
+
const GotInspectorTabsMessage = m('GotInspectorTabsMessage', {
|
|
58
|
+
message: S.Unknown,
|
|
59
|
+
});
|
|
60
|
+
const ReceivedStoreUpdate = m('ReceivedStoreUpdate', {
|
|
61
|
+
entries: S.Array(DisplayEntry),
|
|
62
|
+
startIndex: S.Number,
|
|
63
|
+
isPaused: S.Boolean,
|
|
64
|
+
pausedAtIndex: S.Number,
|
|
65
|
+
});
|
|
66
|
+
const Message = S.Union(ClickedToggle, ClickedRow, ClickedResume, ClickedClear, ClickedFollowLatest, CompletedJump, CompletedResume, CompletedClear, CompletedScroll, ReceivedInspectedState, ToggledTreeNode, GotInspectorTabsMessage, ReceivedStoreUpdate);
|
|
67
|
+
// HELPERS
|
|
68
|
+
const MILLIS_PER_SECOND = 1000;
|
|
69
|
+
const TREE_INDENT_PX = 12;
|
|
70
|
+
const MAX_PREVIEW_KEYS = 3;
|
|
71
|
+
const formatTimeDelta = (deltaMs) => M.value(deltaMs).pipe(M.when(0, () => '0ms'), M.when(Number_.lessThan(MILLIS_PER_SECOND), ms => `+${Math.round(ms)}ms`), M.orElse(ms => `+${(ms / MILLIS_PER_SECOND).toFixed(1)}s`));
|
|
72
|
+
const MESSAGE_LIST_SELECTOR = '.message-list';
|
|
73
|
+
const toDisplayEntries = ({ entries }) => Array_.map(entries, ({ tag, commandCount, timestamp, isModelChanged }) => ({
|
|
74
|
+
tag,
|
|
75
|
+
commandCount,
|
|
76
|
+
timestamp,
|
|
77
|
+
isModelChanged,
|
|
78
|
+
}));
|
|
79
|
+
const toDisplayState = (state) => ({
|
|
80
|
+
entries: toDisplayEntries(state),
|
|
81
|
+
startIndex: state.startIndex,
|
|
82
|
+
isPaused: state.isPaused,
|
|
83
|
+
pausedAtIndex: state.pausedAtIndex,
|
|
84
|
+
});
|
|
85
|
+
const isExpandable = (value) => Predicate.isNotNull(value) && typeof value === 'object';
|
|
86
|
+
const Tagged = S.Struct({ _tag: S.String });
|
|
87
|
+
const isTagged = S.is(Tagged);
|
|
88
|
+
const objectPreview = (value) => pipe(value, Record.keys, Array_.filter(key => key !== '_tag'), Array_.match({
|
|
89
|
+
onEmpty: () => '{}',
|
|
90
|
+
onNonEmpty: keys => {
|
|
91
|
+
const preview = pipe(keys, Array_.take(MAX_PREVIEW_KEYS), Array_.join(', '));
|
|
92
|
+
return Array_.length(keys) > MAX_PREVIEW_KEYS
|
|
93
|
+
? `{ ${preview}, … }`
|
|
94
|
+
: `{ ${preview} }`;
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
const collapsedPreview = (value) => M.value(value).pipe(M.when(Array.isArray, array => `(${array.length})`), M.when(Predicate.isReadonlyRecord, objectPreview), M.orElse(() => ''));
|
|
98
|
+
const emptyDiff = {
|
|
99
|
+
changedPaths: HashSet.empty(),
|
|
100
|
+
affectedPaths: HashSet.empty(),
|
|
101
|
+
};
|
|
102
|
+
const computeDiff = (previous, current) => {
|
|
103
|
+
const changed = new Set();
|
|
104
|
+
const walk = (prev, curr, path) => {
|
|
105
|
+
if (prev === curr) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isExpandable(curr) || !isExpandable(prev)) {
|
|
109
|
+
changed.add(path);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(curr) && Array.isArray(prev)) {
|
|
113
|
+
walkArray(prev, curr, path);
|
|
114
|
+
}
|
|
115
|
+
else if (Predicate.isReadonlyRecord(curr) &&
|
|
116
|
+
Predicate.isReadonlyRecord(prev)) {
|
|
117
|
+
walkObject(prev, curr, path);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
changed.add(path);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const walkObject = (prev, curr, path) => {
|
|
124
|
+
pipe(curr, Record.keys, Array_.forEach(key => {
|
|
125
|
+
const childPath = `${path}.${key}`;
|
|
126
|
+
if (Record.has(prev, key)) {
|
|
127
|
+
walk(prev[key], curr[key], childPath);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
changed.add(childPath);
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
};
|
|
134
|
+
const walkArray = (prev, curr, path) => {
|
|
135
|
+
const prevRefToIndex = new Map(prev.map((item, index) => [item, index]));
|
|
136
|
+
curr.forEach((item, index) => {
|
|
137
|
+
const childPath = `${path}.${index}`;
|
|
138
|
+
const prevIndex = prevRefToIndex.get(item);
|
|
139
|
+
if (Predicate.isUndefined(prevIndex) || prevIndex !== index) {
|
|
140
|
+
changed.add(childPath);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
walk(previous, current, 'root');
|
|
145
|
+
const affected = new Set(changed);
|
|
146
|
+
const addAncestors = (path) => {
|
|
147
|
+
pipe(path, String_.lastIndexOf('.'), Option.map(lastDot => path.substring(0, lastDot)), Option.filter(parent => !affected.has(parent)), Option.map(parent => {
|
|
148
|
+
affected.add(parent);
|
|
149
|
+
addAncestors(parent);
|
|
150
|
+
}));
|
|
151
|
+
};
|
|
152
|
+
changed.forEach(addAncestors);
|
|
153
|
+
return {
|
|
154
|
+
changedPaths: HashSet.fromIterable(changed),
|
|
155
|
+
affectedPaths: HashSet.fromIterable(affected),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
// UPDATE
|
|
159
|
+
const makeUpdate = (store, shadow, mode) => {
|
|
160
|
+
const jumpTo = (index) => Effect.gen(function* () {
|
|
161
|
+
yield* store.jumpTo(index);
|
|
162
|
+
return CompletedJump();
|
|
163
|
+
});
|
|
164
|
+
const inspectState = (index) => Effect.gen(function* () {
|
|
165
|
+
const model = yield* store.getModelAtIndex(index);
|
|
166
|
+
const maybeMessage = yield* store.getMessageAtIndex(index);
|
|
167
|
+
const diff = index === INIT_INDEX
|
|
168
|
+
? emptyDiff
|
|
169
|
+
: yield* pipe(store.getModelAtIndex(index - 1), Effect.map(previousModel => computeDiff(previousModel, model)), Effect.catchAll(() => Effect.succeed(emptyDiff)));
|
|
170
|
+
return ReceivedInspectedState({ model, maybeMessage, ...diff });
|
|
171
|
+
});
|
|
172
|
+
const inspectLatest = Effect.gen(function* () {
|
|
173
|
+
const state = yield* SubscriptionRef.get(store.stateRef);
|
|
174
|
+
const latestIndex = Array_.isEmptyReadonlyArray(state.entries)
|
|
175
|
+
? INIT_INDEX
|
|
176
|
+
: state.startIndex + state.entries.length - 1;
|
|
177
|
+
return yield* inspectState(latestIndex);
|
|
178
|
+
});
|
|
179
|
+
const resume = Effect.gen(function* () {
|
|
180
|
+
yield* store.resume;
|
|
181
|
+
return CompletedResume();
|
|
182
|
+
});
|
|
183
|
+
const clear = Effect.gen(function* () {
|
|
184
|
+
yield* store.clear;
|
|
185
|
+
return CompletedClear();
|
|
186
|
+
});
|
|
187
|
+
const scrollToTop = Effect.sync(() => {
|
|
188
|
+
const messageList = shadow.querySelector(MESSAGE_LIST_SELECTOR);
|
|
189
|
+
if (messageList instanceof HTMLElement) {
|
|
190
|
+
messageList.scrollTop = 0;
|
|
191
|
+
}
|
|
192
|
+
return CompletedScroll();
|
|
193
|
+
});
|
|
194
|
+
return (model, message) => M.value(message).pipe(M.withReturnType(), M.tags({
|
|
195
|
+
ClickedToggle: () => [
|
|
196
|
+
evo(model, {
|
|
197
|
+
isOpen: isOpen => !isOpen,
|
|
198
|
+
}),
|
|
199
|
+
[],
|
|
200
|
+
],
|
|
201
|
+
ClickedRow: ({ index }) => M.value(mode).pipe(M.withReturnType(), M.when('TimeTravel', () => [
|
|
202
|
+
model,
|
|
203
|
+
[jumpTo(index), inspectState(index)],
|
|
204
|
+
]), M.when('Inspect', () => [
|
|
205
|
+
evo(model, {
|
|
206
|
+
maybeSelectedIndex: () => Option.some(index),
|
|
207
|
+
}),
|
|
208
|
+
[inspectState(index)],
|
|
209
|
+
]), M.exhaustive),
|
|
210
|
+
ClickedResume: () => [
|
|
211
|
+
evo(model, {
|
|
212
|
+
expandedPaths: () => HashSet.empty(),
|
|
213
|
+
changedPaths: () => HashSet.empty(),
|
|
214
|
+
affectedPaths: () => HashSet.empty(),
|
|
215
|
+
}),
|
|
216
|
+
[resume, inspectLatest],
|
|
217
|
+
],
|
|
218
|
+
ClickedClear: () => [
|
|
219
|
+
evo(model, {
|
|
220
|
+
maybeSelectedIndex: () => Option.none(),
|
|
221
|
+
maybeInspectedModel: () => Option.none(),
|
|
222
|
+
maybeInspectedMessage: () => Option.none(),
|
|
223
|
+
expandedPaths: () => HashSet.empty(),
|
|
224
|
+
changedPaths: () => HashSet.empty(),
|
|
225
|
+
affectedPaths: () => HashSet.empty(),
|
|
226
|
+
}),
|
|
227
|
+
[clear],
|
|
228
|
+
],
|
|
229
|
+
ClickedFollowLatest: () => [
|
|
230
|
+
evo(model, {
|
|
231
|
+
maybeSelectedIndex: () => Option.none(),
|
|
232
|
+
expandedPaths: () => HashSet.empty(),
|
|
233
|
+
changedPaths: () => HashSet.empty(),
|
|
234
|
+
affectedPaths: () => HashSet.empty(),
|
|
235
|
+
}),
|
|
236
|
+
[inspectLatest, scrollToTop],
|
|
237
|
+
],
|
|
238
|
+
ReceivedInspectedState: ({ model: inspectedModel, maybeMessage, changedPaths, affectedPaths, }) => [
|
|
239
|
+
evo(model, {
|
|
240
|
+
maybeInspectedModel: () => Option.some(inspectedModel),
|
|
241
|
+
maybeInspectedMessage: () => maybeMessage,
|
|
242
|
+
changedPaths: () => changedPaths,
|
|
243
|
+
affectedPaths: () => affectedPaths,
|
|
244
|
+
}),
|
|
245
|
+
[],
|
|
246
|
+
],
|
|
247
|
+
GotInspectorTabsMessage: ({ message: tabsMessage }) => {
|
|
248
|
+
const [nextTabsModel, tabsCommands] = Tabs.update(model.inspectorTabs,
|
|
249
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
250
|
+
tabsMessage);
|
|
251
|
+
return [
|
|
252
|
+
evo(model, {
|
|
253
|
+
inspectorTabs: () => nextTabsModel,
|
|
254
|
+
}),
|
|
255
|
+
tabsCommands.map(Effect.map(innerMessage => GotInspectorTabsMessage({ message: innerMessage }))),
|
|
256
|
+
];
|
|
257
|
+
},
|
|
258
|
+
ToggledTreeNode: ({ path }) => [
|
|
259
|
+
evo(model, {
|
|
260
|
+
expandedPaths: paths => HashSet.toggle(paths, path),
|
|
261
|
+
}),
|
|
262
|
+
[],
|
|
263
|
+
],
|
|
264
|
+
ReceivedStoreUpdate: ({ entries, startIndex, isPaused, pausedAtIndex, }) => {
|
|
265
|
+
const shouldFollowLatest = M.value(mode).pipe(M.when('TimeTravel', () => !isPaused), M.when('Inspect', () => Option.isNone(model.maybeSelectedIndex)), M.exhaustive);
|
|
266
|
+
return [
|
|
267
|
+
evo(model, {
|
|
268
|
+
entries: () => entries,
|
|
269
|
+
startIndex: () => startIndex,
|
|
270
|
+
isPaused: () => isPaused,
|
|
271
|
+
pausedAtIndex: () => pausedAtIndex,
|
|
272
|
+
}),
|
|
273
|
+
shouldFollowLatest ? [scrollToTop, inspectLatest] : [],
|
|
274
|
+
];
|
|
275
|
+
},
|
|
276
|
+
}), M.tag('CompletedJump', 'CompletedResume', 'CompletedClear', 'CompletedScroll', () => [model, []]), M.exhaustive);
|
|
277
|
+
};
|
|
278
|
+
// SUBSCRIPTION
|
|
279
|
+
const SubscriptionDeps = S.Struct({
|
|
280
|
+
storeUpdates: S.Null,
|
|
281
|
+
});
|
|
282
|
+
const makeOverlaySubscriptions = (store) => makeSubscriptions(SubscriptionDeps)({
|
|
283
|
+
storeUpdates: {
|
|
284
|
+
modelToDependencies: () => null,
|
|
285
|
+
depsToStream: () => Stream.concat(Stream.make(pipe(SubscriptionRef.get(store.stateRef), Effect.map(state => ReceivedStoreUpdate(toDisplayState(state))))), pipe(store.stateRef.changes, Stream.map(state => Effect.succeed(ReceivedStoreUpdate(toDisplayState(state)))))),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
// VIEW
|
|
289
|
+
const indexClass = 'text-2xs text-dt-muted font-mono min-w-5';
|
|
290
|
+
const headerButtonClass = 'dt-header-button bg-transparent border-none text-dt-muted cursor-pointer text-base font-mono transition-colors';
|
|
291
|
+
const ROW_BASE = 'dt-row flex items-center py-1 px-1 cursor-pointer gap-1.5 transition-colors border-l-3 border-b';
|
|
292
|
+
const BADGE_POSITION_CLASS = {
|
|
293
|
+
BottomRight: 'dt-pos-br',
|
|
294
|
+
BottomLeft: 'dt-pos-bl',
|
|
295
|
+
TopRight: 'dt-pos-tr',
|
|
296
|
+
TopLeft: 'dt-pos-tl',
|
|
297
|
+
};
|
|
298
|
+
const PANEL_POSITION_CLASS = {
|
|
299
|
+
BottomRight: 'dt-panel-br',
|
|
300
|
+
BottomLeft: 'dt-panel-bl',
|
|
301
|
+
TopRight: 'dt-panel-tr',
|
|
302
|
+
TopLeft: 'dt-panel-tl',
|
|
303
|
+
};
|
|
304
|
+
const makeView = (position, mode, maybeMessage) => {
|
|
305
|
+
const { div, span, button, svg, path, Class, Style, OnClick, AriaHidden, Xmlns, Fill, ViewBox, StrokeWidth, Stroke, StrokeLinecap, StrokeLinejoin, D, } = html();
|
|
306
|
+
// JSON TREE
|
|
307
|
+
const leafValueView = (value) => M.value(value).pipe(M.when(Predicate.isNull, () => span([Class('json-null italic')], ['null'])), M.when(Predicate.isUndefined, () => span([Class('json-null italic')], ['undefined'])), M.when(Predicate.isString, stringValue => span([Class('json-string')], [`"${stringValue}"`])), M.when(Predicate.isNumber, numberValue => span([Class('json-number')], [String(numberValue)])), M.when(Predicate.isBoolean, booleanValue => span([Class('json-boolean')], [String(booleanValue)])), M.orElse(unknownValue => span([Class('json-null')], [String(unknownValue)])));
|
|
308
|
+
const keyView = (key) => span([Class('json-key')], [`${key}:\u00a0`]);
|
|
309
|
+
const CHEVRON_RIGHT = 'M8.25 4.5l7.5 7.5-7.5 7.5';
|
|
310
|
+
const CHEVRON_DOWN = 'M19.5 8.25l-7.5 7.5-7.5-7.5';
|
|
311
|
+
const arrowView = (isExpanded) => svg([
|
|
312
|
+
AriaHidden(true),
|
|
313
|
+
Class('json-arrow shrink-0'),
|
|
314
|
+
Xmlns('http://www.w3.org/2000/svg'),
|
|
315
|
+
Fill('none'),
|
|
316
|
+
ViewBox('0 0 24 24'),
|
|
317
|
+
StrokeWidth('2'),
|
|
318
|
+
Stroke('currentColor'),
|
|
319
|
+
], [
|
|
320
|
+
path([
|
|
321
|
+
StrokeLinecap('round'),
|
|
322
|
+
StrokeLinejoin('round'),
|
|
323
|
+
D(isExpanded ? CHEVRON_DOWN : CHEVRON_RIGHT),
|
|
324
|
+
], []),
|
|
325
|
+
]);
|
|
326
|
+
const tagLabelView = (tag) => span([Class('json-tag')], [tag]);
|
|
327
|
+
const diffDotView = span([Class('diff-dot')], []);
|
|
328
|
+
const inlineDiffDotView = span([Class('diff-dot-inline')], []);
|
|
329
|
+
const flattenTree = (value, treePath, expandedPaths, changedPaths, affectedPaths, depth, maybeKey, accumulator, indentRootChildren) => {
|
|
330
|
+
const isRoot = treePath === 'root';
|
|
331
|
+
const nodeIsExpandable = isExpandable(value);
|
|
332
|
+
const isExpanded = nodeIsExpandable && (isRoot || HashSet.has(expandedPaths, treePath));
|
|
333
|
+
const maybeTag = pipe(value, Option.liftPredicate(isTagged), Option.map(({ _tag }) => _tag));
|
|
334
|
+
accumulator.push({
|
|
335
|
+
value,
|
|
336
|
+
treePath,
|
|
337
|
+
depth,
|
|
338
|
+
maybeKey,
|
|
339
|
+
isExpandable: nodeIsExpandable,
|
|
340
|
+
isExpanded,
|
|
341
|
+
isChanged: HashSet.has(changedPaths, treePath),
|
|
342
|
+
isAffected: HashSet.has(affectedPaths, treePath),
|
|
343
|
+
maybeTag,
|
|
344
|
+
});
|
|
345
|
+
if (!isExpanded) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const childDepth = isRoot && !indentRootChildren ? depth : depth + 1;
|
|
349
|
+
if (Array.isArray(value)) {
|
|
350
|
+
value.forEach((item, arrayIndex) => flattenTree(item, `${treePath}.${arrayIndex}`, expandedPaths, changedPaths, affectedPaths, childDepth, Option.some(String(arrayIndex)), accumulator, indentRootChildren));
|
|
351
|
+
}
|
|
352
|
+
else if (Predicate.isReadonlyRecord(value)) {
|
|
353
|
+
pipe(value, Record.toEntries, Array_.filter(([key]) => key !== '_tag'), Array_.forEach(([key, childValue]) => flattenTree(childValue, `${treePath}.${key}`, expandedPaths, changedPaths, affectedPaths, childDepth, Option.some(key), accumulator, indentRootChildren)));
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const flatNodeView = (node) => {
|
|
357
|
+
const indent = Style({ paddingLeft: `${node.depth * TREE_INDENT_PX}px` });
|
|
358
|
+
const hasDiffDot = node.isChanged || node.isAffected;
|
|
359
|
+
if (!node.isExpandable) {
|
|
360
|
+
return div([
|
|
361
|
+
Class(clsx('tree-row flex items-center gap-px font-mono text-2xs', node.isChanged && 'diff-changed')),
|
|
362
|
+
indent,
|
|
363
|
+
], [
|
|
364
|
+
...(hasDiffDot ? [diffDotView] : []),
|
|
365
|
+
...Array_.getSomes([Option.map(node.maybeKey, keyView)]),
|
|
366
|
+
leafValueView(node.value),
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
const isRoot = node.treePath === 'root';
|
|
370
|
+
const preview = node.isExpanded
|
|
371
|
+
? Array.isArray(node.value)
|
|
372
|
+
? `(${node.value.length})`
|
|
373
|
+
: ''
|
|
374
|
+
: collapsedPreview(node.value);
|
|
375
|
+
return div([
|
|
376
|
+
Class(clsx('tree-row flex items-center gap-px font-mono text-2xs', !isRoot && 'tree-row-expandable cursor-pointer', node.isChanged && 'diff-changed')),
|
|
377
|
+
indent,
|
|
378
|
+
...(isRoot ? [] : [OnClick(ToggledTreeNode({ path: node.treePath }))]),
|
|
379
|
+
], [
|
|
380
|
+
...(isRoot ? [] : [arrowView(node.isExpanded)]),
|
|
381
|
+
...(!isRoot && hasDiffDot ? [diffDotView] : []),
|
|
382
|
+
...Array_.getSomes([
|
|
383
|
+
Option.map(node.maybeKey, keyView),
|
|
384
|
+
Option.map(node.maybeTag, tagLabelView),
|
|
385
|
+
]),
|
|
386
|
+
span([Class('json-preview')], [preview]),
|
|
387
|
+
]);
|
|
388
|
+
};
|
|
389
|
+
const treeView = (value, rootPath, expandedPaths, changedPaths, affectedPaths, maybeRootLabel, indentRootChildren) => {
|
|
390
|
+
const nodes = [];
|
|
391
|
+
flattenTree(value, rootPath, expandedPaths, changedPaths, affectedPaths, 0, maybeRootLabel, nodes, indentRootChildren);
|
|
392
|
+
return div([Class('inspector-tree flex-1 overflow-auto min-h-0 min-w-0')], nodes.map(flatNodeView));
|
|
393
|
+
};
|
|
394
|
+
const inspectedTimestamp = (model) => {
|
|
395
|
+
const lastIndex = Array_.isEmptyReadonlyArray(model.entries)
|
|
396
|
+
? INIT_INDEX
|
|
397
|
+
: model.startIndex + model.entries.length - 1;
|
|
398
|
+
const selectedIndex = M.value(mode).pipe(M.when('TimeTravel', () => model.isPaused ? model.pausedAtIndex : lastIndex), M.when('Inspect', () => Option.getOrElse(model.maybeSelectedIndex, () => lastIndex)), M.exhaustive);
|
|
399
|
+
if (selectedIndex === INIT_INDEX) {
|
|
400
|
+
return '0ms';
|
|
401
|
+
}
|
|
402
|
+
const baseTimestamp = pipe(model.entries, Array_.head, Option.match({
|
|
403
|
+
onNone: () => 0,
|
|
404
|
+
onSome: ({ timestamp }) => timestamp,
|
|
405
|
+
}));
|
|
406
|
+
return pipe(Array_.get(model.entries, selectedIndex - model.startIndex), Option.map(entry => {
|
|
407
|
+
const delta = entry.timestamp - baseTimestamp;
|
|
408
|
+
const seconds = Math.floor(delta / MILLIS_PER_SECOND);
|
|
409
|
+
const remainingMs = delta % MILLIS_PER_SECOND;
|
|
410
|
+
return seconds > 0
|
|
411
|
+
? `+${seconds}s ${remainingMs.toFixed(1)}ms`
|
|
412
|
+
: `+${remainingMs.toFixed(1)}ms`;
|
|
413
|
+
}), Option.getOrElse(() => ''));
|
|
414
|
+
};
|
|
415
|
+
const emptyInspectorView = div([
|
|
416
|
+
Class('flex-1 flex items-center justify-center text-dt-muted text-2xs font-mono min-w-0'),
|
|
417
|
+
], ['Click a message to inspect']);
|
|
418
|
+
const INSPECTOR_TABS = ['Model', 'Message'];
|
|
419
|
+
const noMessageView = div([
|
|
420
|
+
Class('flex-1 flex items-center justify-center text-dt-muted text-2xs font-mono min-w-0'),
|
|
421
|
+
], ['Init — no Message']);
|
|
422
|
+
const modelTabContent = (model, inspectedModel) => treeView(inspectedModel, 'root', model.expandedPaths, model.changedPaths, model.affectedPaths, Option.none(), true);
|
|
423
|
+
const messageTabContent = (model) => Option.match(model.maybeInspectedMessage, {
|
|
424
|
+
onNone: () => noMessageView,
|
|
425
|
+
onSome: message => div([Class('flex flex-col flex-1 min-h-0 min-w-0')], [
|
|
426
|
+
div([
|
|
427
|
+
Class('px-2 py-1 border-b text-2xs text-dt-muted font-mono shrink-0'),
|
|
428
|
+
], [inspectedTimestamp(model)]),
|
|
429
|
+
div([Class('flex flex-col flex-1 min-h-0 min-w-0 pt-1 pl-1')], [
|
|
430
|
+
treeView(message, 'root', model.expandedPaths, HashSet.empty(), HashSet.empty(), Option.none(), false),
|
|
431
|
+
]),
|
|
432
|
+
]),
|
|
433
|
+
});
|
|
434
|
+
const inspectorTabContent = (model, tab, inspectedModel) => M.value(tab).pipe(M.when('Model', () => modelTabContent(model, inspectedModel)), M.when('Message', () => messageTabContent(model)), M.exhaustive);
|
|
435
|
+
const inspectorPaneView = (model) => div([Class('flex flex-col border-l min-w-0 flex-1 dt-inspector-pane')], [
|
|
436
|
+
Tabs.view({
|
|
437
|
+
model: model.inspectorTabs,
|
|
438
|
+
toMessage: tabsMessage => GotInspectorTabsMessage({ message: tabsMessage }),
|
|
439
|
+
tabs: INSPECTOR_TABS,
|
|
440
|
+
className: 'flex flex-col flex-1 min-h-0',
|
|
441
|
+
tabListClassName: 'flex border-b shrink-0',
|
|
442
|
+
tabToConfig: (tab, { isActive }) => ({
|
|
443
|
+
buttonClassName: clsx('dt-tab-button cursor-pointer text-base font-mono px-3 py-1', isActive ? 'text-dt dt-tab-active' : 'text-dt-muted'),
|
|
444
|
+
buttonContent: span([Class('dt-tab-label')], [
|
|
445
|
+
...(isActive ? [span([Class('dt-tab-arrow')], ['→'])] : []),
|
|
446
|
+
tab,
|
|
447
|
+
]),
|
|
448
|
+
panelClassName: 'flex flex-col flex-1 min-h-0 min-w-0',
|
|
449
|
+
panelContent: Option.match(model.maybeInspectedModel, {
|
|
450
|
+
onNone: () => emptyInspectorView,
|
|
451
|
+
onSome: inspectedModel => inspectorTabContent(model, tab, inspectedModel),
|
|
452
|
+
}),
|
|
453
|
+
}),
|
|
454
|
+
}),
|
|
455
|
+
]);
|
|
456
|
+
// MESSAGE LIST
|
|
457
|
+
const badgeView = (model) => button([
|
|
458
|
+
Class(clsx('fixed bg-dt-bg text-dt cursor-pointer flex flex-col items-center justify-center font-mono outline-none dt-badge', BADGE_POSITION_CLASS[position], model.isPaused ? 'dt-badge-paused' : 'dt-badge-accent')),
|
|
459
|
+
Style({ width: '22px', height: '56px', fontSize: '10px' }),
|
|
460
|
+
OnClick(ClickedToggle()),
|
|
461
|
+
], [
|
|
462
|
+
model.isOpen
|
|
463
|
+
? svg([
|
|
464
|
+
AriaHidden(true),
|
|
465
|
+
Xmlns('http://www.w3.org/2000/svg'),
|
|
466
|
+
Fill('none'),
|
|
467
|
+
ViewBox('0 0 24 24'),
|
|
468
|
+
StrokeWidth('1.5'),
|
|
469
|
+
Stroke('currentColor'),
|
|
470
|
+
Style({ width: '12px', height: '12px' }),
|
|
471
|
+
], [
|
|
472
|
+
path([
|
|
473
|
+
StrokeLinecap('round'),
|
|
474
|
+
StrokeLinejoin('round'),
|
|
475
|
+
D('M6 18L18 6M6 6l12 12'),
|
|
476
|
+
], []),
|
|
477
|
+
])
|
|
478
|
+
: div([
|
|
479
|
+
Class('flex flex-col items-center gap-0.5 text-dt-muted font-semibold tracking-wider'),
|
|
480
|
+
], [span([], ['D']), span([], ['E']), span([], ['V'])]),
|
|
481
|
+
]);
|
|
482
|
+
const headerClass = 'flex items-center justify-between px-3 py-1.5 border-b shrink-0';
|
|
483
|
+
const liveHeaderView = div([Class(headerClass)], [
|
|
484
|
+
span([Class('text-base text-dt-live font-medium font-mono')], ['Live']),
|
|
485
|
+
button([Class(headerButtonClass), OnClick(ClickedClear())], ['Clear history']),
|
|
486
|
+
]);
|
|
487
|
+
const pausedHeaderView = (model) => div([Class(headerClass)], [
|
|
488
|
+
button([
|
|
489
|
+
Class('dt-resume-button bg-transparent border-none text-dt-live cursor-pointer text-base font-mono font-medium'),
|
|
490
|
+
OnClick(ClickedResume()),
|
|
491
|
+
], ['Resume →']),
|
|
492
|
+
span([Class('text-base text-dt-paused font-mono')], [
|
|
493
|
+
model.pausedAtIndex === INIT_INDEX
|
|
494
|
+
? 'Paused (init)'
|
|
495
|
+
: `Paused (${model.pausedAtIndex + 1})`,
|
|
496
|
+
]),
|
|
497
|
+
button([Class(headerButtonClass), OnClick(ClickedClear())], ['Clear history']),
|
|
498
|
+
]);
|
|
499
|
+
const inspectingHeaderView = (selectedIndex) => div([Class(headerClass)], [
|
|
500
|
+
button([
|
|
501
|
+
Class('dt-resume-button bg-transparent border-none text-dt-live cursor-pointer text-base font-mono font-medium'),
|
|
502
|
+
OnClick(ClickedFollowLatest()),
|
|
503
|
+
], ['Follow latest →']),
|
|
504
|
+
span([Class('text-base text-dt-accent font-mono')], [
|
|
505
|
+
selectedIndex === INIT_INDEX
|
|
506
|
+
? 'Inspecting (init)'
|
|
507
|
+
: `Inspecting (${selectedIndex + 1})`,
|
|
508
|
+
]),
|
|
509
|
+
button([Class(headerButtonClass), OnClick(ClickedClear())], ['Clear history']),
|
|
510
|
+
]);
|
|
511
|
+
const headerView = (model) => M.value(mode).pipe(M.when('Inspect', () => Option.match(model.maybeSelectedIndex, {
|
|
512
|
+
onNone: () => liveHeaderView,
|
|
513
|
+
onSome: inspectingHeaderView,
|
|
514
|
+
})), M.when('TimeTravel', () => model.isPaused ? pausedHeaderView(model) : liveHeaderView), M.exhaustive);
|
|
515
|
+
const initRowView = (isSelected, isPausedHere) => div([
|
|
516
|
+
Class(clsx(ROW_BASE, isSelected && 'selected')),
|
|
517
|
+
OnClick(ClickedRow({ index: INIT_INDEX })),
|
|
518
|
+
], [
|
|
519
|
+
span([Class('pause-column')], isPausedHere ? [pauseIconView] : []),
|
|
520
|
+
span([Class('dot-column')], []),
|
|
521
|
+
span([Class(indexClass)], []),
|
|
522
|
+
span([Class('text-base text-dt-muted font-mono')], ['Init']),
|
|
523
|
+
]);
|
|
524
|
+
const pauseIconView = svg([
|
|
525
|
+
AriaHidden(true),
|
|
526
|
+
Class('dt-pause-icon'),
|
|
527
|
+
Xmlns('http://www.w3.org/2000/svg'),
|
|
528
|
+
Fill('none'),
|
|
529
|
+
ViewBox('0 0 24 24'),
|
|
530
|
+
StrokeWidth('2.5'),
|
|
531
|
+
Stroke('currentColor'),
|
|
532
|
+
], [
|
|
533
|
+
path([
|
|
534
|
+
StrokeLinecap('round'),
|
|
535
|
+
StrokeLinejoin('round'),
|
|
536
|
+
D('M5.75 3v18M18.25 3v18'),
|
|
537
|
+
], []),
|
|
538
|
+
]);
|
|
539
|
+
const messageRowView = (tag, absoluteIndex, isSelected, isPausedHere, timeDelta, isModelChanged) => div([
|
|
540
|
+
Class(clsx(ROW_BASE, isSelected && 'selected')),
|
|
541
|
+
OnClick(ClickedRow({ index: absoluteIndex })),
|
|
542
|
+
], [
|
|
543
|
+
span([Class('pause-column')], isPausedHere ? [pauseIconView] : []),
|
|
544
|
+
span([Class('dot-column')], isModelChanged ? [inlineDiffDotView] : []),
|
|
545
|
+
span([Class(indexClass)], [String(absoluteIndex + 1)]),
|
|
546
|
+
span([Class('text-base text-dt font-mono flex-1 truncate')], [tag]),
|
|
547
|
+
span([
|
|
548
|
+
Class('text-2xs text-dt-muted font-mono shrink-0 text-right min-w-5'),
|
|
549
|
+
], [formatTimeDelta(timeDelta)]),
|
|
550
|
+
]);
|
|
551
|
+
const messageListView = (model) => {
|
|
552
|
+
const baseTimestamp = pipe(model.entries, Array_.head, Option.match({
|
|
553
|
+
onNone: () => 0,
|
|
554
|
+
onSome: ({ timestamp }) => timestamp,
|
|
555
|
+
}));
|
|
556
|
+
const lastIndex = Array_.isEmptyReadonlyArray(model.entries)
|
|
557
|
+
? INIT_INDEX
|
|
558
|
+
: model.startIndex + model.entries.length - 1;
|
|
559
|
+
const selectedIndex = M.value(mode).pipe(M.when('TimeTravel', () => model.isPaused ? model.pausedAtIndex : lastIndex), M.when('Inspect', () => Option.getOrElse(model.maybeSelectedIndex, () => lastIndex)), M.exhaustive);
|
|
560
|
+
const isInitSelected = selectedIndex === INIT_INDEX;
|
|
561
|
+
const messageRows = pipe(model.entries, Array_.map((entry, arrayIndex) => {
|
|
562
|
+
const absoluteIndex = model.startIndex + arrayIndex;
|
|
563
|
+
const isSelected = selectedIndex === absoluteIndex;
|
|
564
|
+
const isPausedHere = model.isPaused && model.pausedAtIndex === absoluteIndex;
|
|
565
|
+
return messageRowView(entry.tag, absoluteIndex, isSelected, isPausedHere, entry.timestamp - baseTimestamp, entry.isModelChanged);
|
|
566
|
+
}), Array_.reverse);
|
|
567
|
+
return div([Class('message-list flex-1 overflow-y-auto min-h-0')], [
|
|
568
|
+
...messageRows,
|
|
569
|
+
initRowView(isInitSelected, model.isPaused && model.pausedAtIndex === INIT_INDEX),
|
|
570
|
+
]);
|
|
571
|
+
};
|
|
572
|
+
// PANEL
|
|
573
|
+
const panelView = (model) => div([
|
|
574
|
+
Class(clsx('fixed dt-panel dt-panel-wide bg-dt-bg border rounded-lg flex flex-col overflow-hidden font-mono text-dt', PANEL_POSITION_CLASS[position])),
|
|
575
|
+
], [
|
|
576
|
+
...pipe(maybeMessage, Option.map(message => div([
|
|
577
|
+
Class('px-3 py-1.5 border-b text-sm text-dt-muted font-mono shrink-0 leading-snug'),
|
|
578
|
+
], [message])), Option.toArray),
|
|
579
|
+
headerView(model),
|
|
580
|
+
div([Class('flex flex-1 min-h-0 dt-content')], [
|
|
581
|
+
div([Class('flex flex-col min-h-0 dt-message-pane')], [messageListView(model)]),
|
|
582
|
+
inspectorPaneView(model),
|
|
583
|
+
]),
|
|
584
|
+
]);
|
|
585
|
+
const interactionBlocker = div([Class('dt-interaction-blocker')], []);
|
|
586
|
+
return (model) => div([], [
|
|
587
|
+
...pipe(OptionExt.when(model.isPaused && mode === 'TimeTravel', interactionBlocker), Option.toArray),
|
|
588
|
+
...pipe(OptionExt.when(model.isOpen, panelView(model)), Option.toArray),
|
|
589
|
+
badgeView(model),
|
|
590
|
+
]);
|
|
591
|
+
};
|
|
592
|
+
// CREATE
|
|
593
|
+
export const createOverlay = (store, position, mode, maybeMessage) => Effect.gen(function* () {
|
|
594
|
+
const existingHost = document.getElementById('foldkit-devtools');
|
|
595
|
+
if (existingHost) {
|
|
596
|
+
existingHost.remove();
|
|
597
|
+
}
|
|
598
|
+
const host = document.createElement('div');
|
|
599
|
+
host.id = 'foldkit-devtools';
|
|
600
|
+
document.body.appendChild(host);
|
|
601
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
602
|
+
const styleElement = document.createElement('style');
|
|
603
|
+
styleElement.textContent = overlayStyles;
|
|
604
|
+
shadow.appendChild(styleElement);
|
|
605
|
+
const container = document.createElement('div');
|
|
606
|
+
shadow.appendChild(container);
|
|
607
|
+
const currentState = yield* SubscriptionRef.get(store.stateRef);
|
|
608
|
+
const init = () => [
|
|
609
|
+
{
|
|
610
|
+
isOpen: false,
|
|
611
|
+
maybeSelectedIndex: Option.none(),
|
|
612
|
+
maybeInspectedModel: Option.none(),
|
|
613
|
+
maybeInspectedMessage: Option.none(),
|
|
614
|
+
expandedPaths: HashSet.empty(),
|
|
615
|
+
changedPaths: HashSet.empty(),
|
|
616
|
+
affectedPaths: HashSet.empty(),
|
|
617
|
+
inspectorTabs: Tabs.init({ id: INSPECTOR_TABS_ID }),
|
|
618
|
+
...toDisplayState(currentState),
|
|
619
|
+
},
|
|
620
|
+
[],
|
|
621
|
+
];
|
|
622
|
+
const overlayRuntime = makeElement({
|
|
623
|
+
Model,
|
|
624
|
+
init,
|
|
625
|
+
update: makeUpdate(store, shadow, mode),
|
|
626
|
+
view: makeView(position, mode, maybeMessage),
|
|
627
|
+
container,
|
|
628
|
+
subscriptions: makeOverlaySubscriptions(store),
|
|
629
|
+
devtools: { show: 'Never' },
|
|
630
|
+
});
|
|
631
|
+
yield* Effect.forkDaemon(overlayRuntime());
|
|
632
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Effect, HashMap, Option, SubscriptionRef } from 'effect';
|
|
2
|
+
export declare const INIT_INDEX = -1;
|
|
3
|
+
export type HistoryEntry = Readonly<{
|
|
4
|
+
tag: string;
|
|
5
|
+
message: unknown;
|
|
6
|
+
commandCount: number;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
isModelChanged: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export type StoreState = Readonly<{
|
|
11
|
+
entries: ReadonlyArray<HistoryEntry>;
|
|
12
|
+
keyframes: HashMap.HashMap<number, unknown>;
|
|
13
|
+
maybeInitModel: Option.Option<unknown>;
|
|
14
|
+
startIndex: number;
|
|
15
|
+
isPaused: boolean;
|
|
16
|
+
pausedAtIndex: number;
|
|
17
|
+
}>;
|
|
18
|
+
export type Bridge = Readonly<{
|
|
19
|
+
replay: (model: unknown, message: unknown) => unknown;
|
|
20
|
+
render: (model: unknown) => Effect.Effect<void>;
|
|
21
|
+
getCurrentModel: Effect.Effect<unknown>;
|
|
22
|
+
}>;
|
|
23
|
+
export declare const createDevtoolsStore: (bridge: Bridge, maxEntries?: number) => Effect.Effect<DevtoolsStore>;
|
|
24
|
+
export type DevtoolsStore = Readonly<{
|
|
25
|
+
recordInit: (model: unknown) => Effect.Effect<void>;
|
|
26
|
+
recordMessage: (message: Readonly<{
|
|
27
|
+
_tag: string;
|
|
28
|
+
}>, modelAfterUpdate: unknown, commandCount: number, isModelChanged: boolean) => Effect.Effect<void>;
|
|
29
|
+
getModelAtIndex: (index: number) => Effect.Effect<unknown>;
|
|
30
|
+
getMessageAtIndex: (index: number) => Effect.Effect<Option.Option<unknown>>;
|
|
31
|
+
jumpTo: (index: number) => Effect.Effect<void>;
|
|
32
|
+
resume: Effect.Effect<void>;
|
|
33
|
+
clear: Effect.Effect<void>;
|
|
34
|
+
stateRef: SubscriptionRef.SubscriptionRef<StoreState>;
|
|
35
|
+
}>;
|
|
36
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/devtools/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAQ,MAAM,QAAQ,CAAA;AAE9E,eAAO,MAAM,UAAU,KAAK,CAAA;AAI5B,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,OAAO,CAAA;CACxB,CAAC,CAAA;AAEF,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IACpC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC3C,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;CACtB,CAAC,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC;IAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;IACrD,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/C,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACxC,CAAC,CAAA;AAWF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,MAAM,EACd,mBAAgC,KAC/B,MAAM,CAAC,MAAM,CAAC,aAAa,CAmJ1B,CAAA;AAEJ,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACnD,aAAa,EAAE,CACb,OAAO,EAAE,QAAQ,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,EACnC,gBAAgB,EAAE,OAAO,EACzB,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,OAAO,KACpB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxB,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC1D,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IAC3E,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC1B,QAAQ,EAAE,eAAe,CAAC,eAAe,CAAC,UAAU,CAAC,CAAA;CACtD,CAAC,CAAA"}
|