@buoy-gg/route-events 7.0.1 → 7.0.2

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 (27) hide show
  1. package/lib/commonjs/components/RouteEventDetail.js +90 -0
  2. package/lib/commonjs/components/RouteEventItemCompact.js +3 -19
  3. package/lib/commonjs/components/RouteEventsModalWithTabs.js +49 -3
  4. package/lib/commonjs/components/RouteEventsTimeline.js +10 -9
  5. package/lib/commonjs/index.js +7 -0
  6. package/lib/commonjs/utils/routeTemplate.js +28 -0
  7. package/lib/module/components/RouteEventDetail.js +86 -0
  8. package/lib/module/components/RouteEventItemCompact.js +1 -17
  9. package/lib/module/components/RouteEventsModalWithTabs.js +49 -3
  10. package/lib/module/components/RouteEventsTimeline.js +11 -10
  11. package/lib/module/index.js +1 -0
  12. package/lib/module/utils/routeTemplate.js +24 -0
  13. package/lib/typescript/components/RouteEventDetail.d.ts +18 -0
  14. package/lib/typescript/components/RouteEventDetail.d.ts.map +1 -0
  15. package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -1
  16. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
  17. package/lib/typescript/components/RouteEventsTimeline.d.ts +4 -2
  18. package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -1
  19. package/lib/typescript/index.d.ts +2 -0
  20. package/lib/typescript/index.d.ts.map +1 -1
  21. package/lib/typescript/utils/routeTemplate.d.ts +8 -0
  22. package/lib/typescript/utils/routeTemplate.d.ts.map +1 -0
  23. package/package.json +5 -5
  24. package/lib/commonjs/components/RouteEventDetailContent.js +0 -499
  25. package/lib/module/components/RouteEventDetailContent.js +0 -494
  26. package/lib/typescript/components/RouteEventDetailContent.d.ts +0 -21
  27. package/lib/typescript/components/RouteEventDetailContent.d.ts.map +0 -1
@@ -1,494 +0,0 @@
1
- "use strict";
2
-
3
- import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
4
- import { useEffect, useState, useCallback, useRef } from "react";
5
- import { formatRelativeTime, devToolsStorageKeys, ChevronLeft, ChevronRight, AlertCircle, Navigation as NavigationIcon, GitBranch, persistentStorage, buoyColors } from "@buoy-gg/shared-ui";
6
- import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
7
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
8
- export function RouteEventDetailContent({
9
- conversation,
10
- selectedEventIndex = 0,
11
- onEventIndexChange = () => {},
12
- disableInternalFooter = false
13
- }) {
14
- // Internal view state
15
- const [internalActiveView, setInternalActiveView] = useState("current");
16
-
17
- // Track if preferences have been loaded
18
- const hasLoadedPreferences = useRef(false);
19
-
20
- // Load saved preferences on mount
21
- useEffect(() => {
22
- if (hasLoadedPreferences.current) return;
23
- const loadPreferences = async () => {
24
- try {
25
- // Load detail view preference (current/diff)
26
- const savedDetailView = await persistentStorage.getItem(devToolsStorageKeys.routeEvents.detailView());
27
- if (savedDetailView === "current" || savedDetailView === "diff") {
28
- setInternalActiveView(savedDetailView);
29
- }
30
- hasLoadedPreferences.current = true;
31
- } catch (error) {
32
- // Failed to load view preferences
33
- }
34
- };
35
- loadPreferences();
36
- }, []);
37
-
38
- // Save detail view preference when changed
39
- const handleViewChange = useCallback(async view => {
40
- setInternalActiveView(view);
41
- try {
42
- await persistentStorage.setItem(devToolsStorageKeys.routeEvents.detailView(), view);
43
- } catch (error) {
44
- // Failed to save detail view preference
45
- }
46
- }, []);
47
- const renderRouteContent = (event, label) => {
48
- return /*#__PURE__*/_jsxs(View, {
49
- style: styles.valueContent,
50
- children: [/*#__PURE__*/_jsx(View, {
51
- style: styles.valueHeader,
52
- children: /*#__PURE__*/_jsx(Text, {
53
- style: styles.valueLabel,
54
- children: label
55
- })
56
- }), /*#__PURE__*/_jsx(View, {
57
- style: styles.dataViewerContainer,
58
- children: /*#__PURE__*/_jsx(DataViewer, {
59
- title: "",
60
- data: {
61
- pathname: event.pathname,
62
- segments: event.segments,
63
- params: event.params,
64
- timestamp: new Date(event.timestamp).toISOString()
65
- },
66
- showTypeFilter: true,
67
- rawMode: true,
68
- initialExpanded: true
69
- })
70
- })]
71
- });
72
- };
73
-
74
- // Get all events sorted by time
75
- const navigationItems = conversation.events.sort((a, b) => a.timestamp - b.timestamp);
76
- const totalEvents = navigationItems.length;
77
-
78
- // Precise time HH:MM:SS.mmm
79
- const formatTimeWithMs = useCallback(timestamp => {
80
- const date = new Date(timestamp);
81
- const h = String(date.getHours()).padStart(2, "0");
82
- const m = String(date.getMinutes()).padStart(2, "0");
83
- const s = String(date.getSeconds()).padStart(2, "0");
84
- const ms = String(date.getMilliseconds()).padStart(3, "0");
85
- return `${h}:${m}:${s}.${ms}`;
86
- }, []);
87
-
88
- // Render current value tab
89
- const renderCurrentValue = () => {
90
- const selectedEvent = navigationItems[selectedEventIndex];
91
- const eventToShow = selectedEvent ?? conversation.lastEvent;
92
- return /*#__PURE__*/_jsx(View, {
93
- style: styles.fullPageSection,
94
- children: /*#__PURE__*/_jsx(View, {
95
- style: styles.contentCard,
96
- children: renderRouteContent(eventToShow, "CURRENT ROUTE STATE")
97
- })
98
- });
99
- };
100
-
101
- // Render diff tab
102
- const renderDiff = () => {
103
- if (navigationItems.length === 0) {
104
- return /*#__PURE__*/_jsxs(View, {
105
- style: styles.emptyState,
106
- children: [/*#__PURE__*/_jsx(AlertCircle, {
107
- size: 32,
108
- color: buoyColors.textMuted
109
- }), /*#__PURE__*/_jsx(Text, {
110
- style: styles.emptyText,
111
- children: "No changes to display"
112
- })]
113
- });
114
- }
115
- const prevIndex = Math.max(0, selectedEventIndex - 1);
116
- const currentIndex = selectedEventIndex;
117
- const previousEvent = navigationItems[prevIndex];
118
- const currentEvent = navigationItems[currentIndex];
119
- return /*#__PURE__*/_jsx(ScrollView, {
120
- style: styles.fullPageSection,
121
- children: /*#__PURE__*/_jsxs(View, {
122
- style: styles.diffContainer,
123
- children: [/*#__PURE__*/_jsxs(View, {
124
- style: styles.diffSection,
125
- children: [/*#__PURE__*/_jsx(Text, {
126
- style: styles.diffSectionTitle,
127
- children: "PREVIOUS"
128
- }), /*#__PURE__*/_jsx(View, {
129
- style: styles.contentCard,
130
- children: renderRouteContent(previousEvent, `Event #${prevIndex + 1}`)
131
- })]
132
- }), /*#__PURE__*/_jsx(View, {
133
- style: styles.diffDivider
134
- }), /*#__PURE__*/_jsxs(View, {
135
- style: styles.diffSection,
136
- children: [/*#__PURE__*/_jsx(Text, {
137
- style: styles.diffSectionTitle,
138
- children: "CURRENT"
139
- }), /*#__PURE__*/_jsx(View, {
140
- style: styles.contentCard,
141
- children: renderRouteContent(currentEvent, `Event #${currentIndex + 1}`)
142
- })]
143
- })]
144
- })
145
- });
146
- };
147
- return /*#__PURE__*/_jsxs(_Fragment, {
148
- children: [/*#__PURE__*/_jsxs(View, {
149
- style: [styles.contentOnly, {
150
- flex: 1,
151
- paddingBottom: !disableInternalFooter && totalEvents > 1 ? 80 : 0
152
- }],
153
- children: [/*#__PURE__*/_jsxs(View, {
154
- style: styles.viewToggleContainer,
155
- children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
156
- style: [styles.viewToggleCard, internalActiveView === "current" && styles.viewToggleCardActive],
157
- onPress: () => handleViewChange("current"),
158
- activeOpacity: 0.8,
159
- children: [/*#__PURE__*/_jsxs(View, {
160
- style: styles.viewToggleContent,
161
- children: [/*#__PURE__*/_jsx(NavigationIcon, {
162
- size: 16,
163
- color: internalActiveView === "current" ? buoyColors.primary : buoyColors.textSecondary
164
- }), /*#__PURE__*/_jsx(Text, {
165
- style: [styles.viewToggleLabel, internalActiveView === "current" && styles.viewToggleLabelActive],
166
- children: "CURRENT STATE"
167
- })]
168
- }), /*#__PURE__*/_jsx(Text, {
169
- style: [styles.viewToggleDescription, internalActiveView === "current" && {
170
- color: buoyColors.text
171
- }],
172
- children: "View the current route state"
173
- })]
174
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
175
- style: [styles.viewToggleCard, internalActiveView === "diff" && styles.viewToggleCardActive],
176
- onPress: () => handleViewChange("diff"),
177
- activeOpacity: 0.8,
178
- children: [/*#__PURE__*/_jsxs(View, {
179
- style: styles.viewToggleContent,
180
- children: [/*#__PURE__*/_jsx(GitBranch, {
181
- size: 16,
182
- color: internalActiveView === "diff" ? buoyColors.success : buoyColors.textSecondary
183
- }), /*#__PURE__*/_jsx(Text, {
184
- style: [styles.viewToggleLabel, internalActiveView === "diff" && styles.viewToggleLabelActive],
185
- children: "DIFF VIEW"
186
- })]
187
- }), /*#__PURE__*/_jsx(Text, {
188
- style: [styles.viewToggleDescription, internalActiveView === "diff" && {
189
- color: buoyColors.text
190
- }],
191
- children: "Compare changes between events"
192
- })]
193
- })]
194
- }), internalActiveView === "current" && renderCurrentValue(), internalActiveView === "diff" && renderDiff()]
195
- }), totalEvents > 1 && !disableInternalFooter && /*#__PURE__*/_jsxs(View, {
196
- style: styles.stickyFooter,
197
- children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
198
- onPress: () => onEventIndexChange(Math.max(0, selectedEventIndex - 1)),
199
- disabled: selectedEventIndex === 0,
200
- style: [styles.navButton, selectedEventIndex === 0 && styles.navButtonDisabled],
201
- children: [/*#__PURE__*/_jsx(ChevronLeft, {
202
- size: 20,
203
- color: selectedEventIndex === 0 ? buoyColors.textMuted : buoyColors.text
204
- }), /*#__PURE__*/_jsx(Text, {
205
- style: [styles.navButtonText, selectedEventIndex === 0 && styles.navButtonTextDisabled],
206
- children: "Previous"
207
- })]
208
- }), /*#__PURE__*/_jsxs(View, {
209
- style: styles.eventCounterContainer,
210
- children: [/*#__PURE__*/_jsxs(Text, {
211
- style: styles.eventCounter,
212
- children: ["Event ", selectedEventIndex + 1, " of ", totalEvents]
213
- }), /*#__PURE__*/_jsx(Text, {
214
- style: styles.eventTimestamp,
215
- children: formatRelativeTime(new Date(navigationItems[selectedEventIndex]?.timestamp))
216
- })]
217
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
218
- onPress: () => onEventIndexChange(Math.min(totalEvents - 1, selectedEventIndex + 1)),
219
- disabled: selectedEventIndex === totalEvents - 1,
220
- style: [styles.navButton, selectedEventIndex === totalEvents - 1 && styles.navButtonDisabled],
221
- children: [/*#__PURE__*/_jsx(Text, {
222
- style: [styles.navButtonText, selectedEventIndex === totalEvents - 1 && styles.navButtonTextDisabled],
223
- children: "Next"
224
- }), /*#__PURE__*/_jsx(ChevronRight, {
225
- size: 20,
226
- color: selectedEventIndex === totalEvents - 1 ? buoyColors.textMuted : buoyColors.text
227
- })]
228
- })]
229
- })]
230
- });
231
- }
232
-
233
- // External footer component to be rendered by the modal outside the ScrollView
234
- export function RouteEventDetailFooter({
235
- conversation,
236
- selectedEventIndex = 0,
237
- onEventIndexChange = () => {}
238
- }) {
239
- const navigationItems = conversation.events.sort((a, b) => a.timestamp - b.timestamp);
240
- const totalEvents = navigationItems.length;
241
- if (totalEvents <= 1) return null;
242
- return /*#__PURE__*/_jsxs(View, {
243
- style: styles.externalFooterBar,
244
- children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
245
- onPress: () => onEventIndexChange(Math.max(0, selectedEventIndex - 1)),
246
- disabled: selectedEventIndex === 0,
247
- style: [styles.navButton, selectedEventIndex === 0 && styles.navButtonDisabled],
248
- children: [/*#__PURE__*/_jsx(ChevronLeft, {
249
- size: 20,
250
- color: selectedEventIndex === 0 ? buoyColors.textMuted : buoyColors.text
251
- }), /*#__PURE__*/_jsx(Text, {
252
- style: [styles.navButtonText, selectedEventIndex === 0 && styles.navButtonTextDisabled],
253
- children: "Previous"
254
- })]
255
- }), /*#__PURE__*/_jsxs(View, {
256
- style: styles.eventCounterContainer,
257
- children: [/*#__PURE__*/_jsxs(Text, {
258
- style: styles.eventCounter,
259
- children: ["Event ", selectedEventIndex + 1, " of ", totalEvents]
260
- }), /*#__PURE__*/_jsx(Text, {
261
- style: styles.eventTimestamp,
262
- children: formatRelativeTime(new Date(navigationItems[selectedEventIndex]?.timestamp))
263
- })]
264
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
265
- onPress: () => onEventIndexChange(Math.min(totalEvents - 1, selectedEventIndex + 1)),
266
- disabled: selectedEventIndex === totalEvents - 1,
267
- style: [styles.navButton, selectedEventIndex === totalEvents - 1 && styles.navButtonDisabled],
268
- children: [/*#__PURE__*/_jsx(Text, {
269
- style: [styles.navButtonText, selectedEventIndex === totalEvents - 1 && styles.navButtonTextDisabled],
270
- children: "Next"
271
- }), /*#__PURE__*/_jsx(ChevronRight, {
272
- size: 20,
273
- color: selectedEventIndex === totalEvents - 1 ? buoyColors.textMuted : buoyColors.text
274
- })]
275
- })]
276
- });
277
- }
278
- const styles = StyleSheet.create({
279
- contentOnly: {
280
- flex: 1,
281
- backgroundColor: buoyColors.base
282
- },
283
- stickyFooter: {
284
- position: "absolute",
285
- bottom: 0,
286
- left: 0,
287
- right: 0,
288
- flexDirection: "row",
289
- alignItems: "center",
290
- justifyContent: "space-between",
291
- paddingHorizontal: 16,
292
- paddingVertical: 12,
293
- backgroundColor: buoyColors.base,
294
- borderTopWidth: 1,
295
- borderTopColor: buoyColors.border,
296
- shadowColor: "#000",
297
- shadowOffset: {
298
- width: 0,
299
- height: -2
300
- },
301
- shadowOpacity: 0.1,
302
- shadowRadius: 4,
303
- elevation: 5
304
- },
305
- externalFooterBar: {
306
- flexDirection: "row",
307
- alignItems: "center",
308
- justifyContent: "space-between",
309
- paddingHorizontal: 16,
310
- paddingVertical: 12,
311
- backgroundColor: buoyColors.base,
312
- borderTopWidth: 1,
313
- borderTopColor: buoyColors.border,
314
- shadowColor: "#000",
315
- shadowOffset: {
316
- width: 0,
317
- height: -2
318
- },
319
- shadowOpacity: 0.1,
320
- shadowRadius: 4,
321
- elevation: 5
322
- },
323
- fullPageSection: {
324
- flex: 1,
325
- paddingHorizontal: 14,
326
- paddingVertical: 10
327
- },
328
- emptyState: {
329
- flex: 1,
330
- alignItems: "center",
331
- justifyContent: "center",
332
- paddingVertical: 48
333
- },
334
- emptyText: {
335
- marginTop: 12,
336
- fontSize: 14,
337
- color: buoyColors.textSecondary,
338
- fontFamily: "monospace"
339
- },
340
- valueContent: {
341
- marginTop: 4
342
- },
343
- valueHeader: {
344
- flexDirection: "row",
345
- alignItems: "center",
346
- justifyContent: "space-between",
347
- marginBottom: 4
348
- },
349
- valueLabel: {
350
- fontSize: 10,
351
- color: buoyColors.textSecondary,
352
- fontFamily: "monospace",
353
- letterSpacing: 0.5,
354
- fontWeight: "600",
355
- textTransform: "uppercase"
356
- },
357
- valueBox: {
358
- backgroundColor: buoyColors.card,
359
- borderRadius: 6,
360
- borderWidth: 1,
361
- borderColor: buoyColors.border,
362
- padding: 8
363
- },
364
- navButton: {
365
- flexDirection: "row",
366
- alignItems: "center",
367
- gap: 6,
368
- paddingHorizontal: 12,
369
- paddingVertical: 8,
370
- borderRadius: 6,
371
- backgroundColor: buoyColors.card,
372
- minWidth: 100,
373
- justifyContent: "center"
374
- },
375
- navButtonDisabled: {
376
- opacity: 0.3
377
- },
378
- navButtonText: {
379
- fontSize: 12,
380
- fontWeight: "600",
381
- color: buoyColors.text,
382
- fontFamily: "monospace",
383
- textTransform: "uppercase"
384
- },
385
- navButtonTextDisabled: {
386
- color: buoyColors.textMuted
387
- },
388
- eventCounterContainer: {
389
- alignItems: "center"
390
- },
391
- eventCounter: {
392
- fontSize: 14,
393
- fontWeight: "700",
394
- color: buoyColors.text,
395
- fontFamily: "monospace",
396
- textTransform: "uppercase"
397
- },
398
- eventTimestamp: {
399
- fontSize: 11,
400
- color: buoyColors.textSecondary,
401
- fontFamily: "monospace",
402
- marginTop: 2
403
- },
404
- contentCard: {
405
- backgroundColor: buoyColors.card,
406
- borderRadius: 14,
407
- padding: 14,
408
- borderWidth: 1,
409
- borderColor: buoyColors.border,
410
- shadowColor: "#000",
411
- shadowOffset: {
412
- width: 0,
413
- height: 2
414
- },
415
- shadowOpacity: 0.04,
416
- shadowRadius: 16,
417
- elevation: 2
418
- },
419
- // View Toggle Cards
420
- viewToggleContainer: {
421
- flexDirection: "row",
422
- gap: 12,
423
- padding: 14,
424
- backgroundColor: buoyColors.base
425
- },
426
- viewToggleCard: {
427
- flex: 1,
428
- backgroundColor: buoyColors.card,
429
- borderRadius: 14,
430
- borderWidth: 1,
431
- borderColor: buoyColors.border,
432
- padding: 14,
433
- gap: 8
434
- },
435
- viewToggleCardActive: {
436
- borderWidth: 1.5,
437
- borderColor: buoyColors.primary,
438
- backgroundColor: buoyColors.primary + "15",
439
- shadowColor: buoyColors.primary,
440
- shadowOffset: {
441
- width: 0,
442
- height: 2
443
- },
444
- shadowOpacity: 0.1,
445
- shadowRadius: 8,
446
- elevation: 3
447
- },
448
- viewToggleContent: {
449
- flexDirection: "row",
450
- alignItems: "center",
451
- gap: 8
452
- },
453
- viewToggleLabel: {
454
- fontSize: 12,
455
- fontWeight: "700",
456
- letterSpacing: 0.5,
457
- color: buoyColors.textSecondary,
458
- textTransform: "uppercase"
459
- },
460
- viewToggleLabelActive: {
461
- color: buoyColors.text
462
- },
463
- viewToggleDescription: {
464
- fontSize: 11,
465
- color: buoyColors.textMuted,
466
- lineHeight: 16
467
- },
468
- // Diff view styles
469
- diffContainer: {
470
- gap: 16
471
- },
472
- diffSection: {
473
- gap: 8
474
- },
475
- diffSectionTitle: {
476
- fontSize: 10,
477
- fontWeight: "700",
478
- color: buoyColors.textSecondary,
479
- fontFamily: "monospace",
480
- letterSpacing: 0.5,
481
- textTransform: "uppercase",
482
- paddingHorizontal: 4
483
- },
484
- diffDivider: {
485
- height: 1,
486
- backgroundColor: buoyColors.border,
487
- marginVertical: 8
488
- },
489
- dataViewerContainer: {
490
- marginTop: -12,
491
- marginHorizontal: -12,
492
- marginBottom: -12
493
- }
494
- });
@@ -1,21 +0,0 @@
1
- import type { RouteChangeEvent } from "../RouteObserver";
2
- interface RouteConversation {
3
- pathname: string;
4
- lastEvent: RouteChangeEvent;
5
- events: RouteChangeEvent[];
6
- totalNavigations: number;
7
- }
8
- interface RouteEventDetailContentProps {
9
- conversation: RouteConversation;
10
- selectedEventIndex?: number;
11
- onEventIndexChange?: (index: number) => void;
12
- disableInternalFooter?: boolean;
13
- }
14
- export declare function RouteEventDetailContent({ conversation, selectedEventIndex, onEventIndexChange, disableInternalFooter, }: RouteEventDetailContentProps): import("react").JSX.Element;
15
- export declare function RouteEventDetailFooter({ conversation, selectedEventIndex, onEventIndexChange, }: {
16
- conversation: RouteConversation;
17
- selectedEventIndex?: number;
18
- onEventIndexChange?: (index: number) => void;
19
- }): import("react").JSX.Element | null;
20
- export {};
21
- //# sourceMappingURL=RouteEventDetailContent.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"RouteEventDetailContent.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventDetailContent.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAczD,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,4BAA4B;IACpC,YAAY,EAAE,iBAAiB,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,uBAAuB,CAAC,EACtC,YAAY,EACZ,kBAAsB,EACtB,kBAA6B,EAC7B,qBAA6B,GAC9B,EAAE,4BAA4B,+BA0T9B;AAGD,wBAAgB,sBAAsB,CAAC,EACrC,YAAY,EACZ,kBAAsB,EACtB,kBAA6B,GAC9B,EAAE;IACD,YAAY,EAAE,iBAAiB,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C,sCA2EA"}