@sentry/junior-dashboard 0.273.0 → 0.275.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.
Files changed (36) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/client.js +76 -76
  3. package/dist/index.js +3 -3
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +4 -4
  6. package/src/client/App.tsx +11 -0
  7. package/src/client/components/Metric.tsx +0 -32
  8. package/src/client/components/ProfileMenu.tsx +56 -4
  9. package/src/client/components/layout/DashboardChrome.tsx +3 -3
  10. package/src/client/components/layout/DashboardHeader.tsx +41 -4
  11. package/src/client/components/layout/NavRail.tsx +43 -40
  12. package/src/client/conversations/ConversationComposer.tsx +7 -4
  13. package/src/client/conversations/ConversationDetails.tsx +117 -162
  14. package/src/client/conversations/ConversationHeader.tsx +119 -133
  15. package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
  16. package/src/client/conversations/ConversationMeta.tsx +49 -44
  17. package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
  18. package/src/client/conversations/ConversationPage.tsx +33 -368
  19. package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
  20. package/src/client/conversations/ConversationSidebar.tsx +119 -60
  21. package/src/client/conversations/ConversationWorkspace.tsx +8 -3
  22. package/src/client/conversations/TelemetryMetrics.tsx +0 -14
  23. package/src/client/conversations/TranscriptView.tsx +15 -32
  24. package/src/client/conversations/UsageCard.tsx +1 -15
  25. package/src/client/conversations/composerAutocorrect.ts +12 -0
  26. package/src/client/conversations/conversationAge.ts +24 -0
  27. package/src/client/conversations/conversationRoutes.ts +14 -5
  28. package/src/client/conversations/conversationSections.ts +66 -48
  29. package/src/client/conversations/useConversationFinishedIndicators.ts +40 -8
  30. package/src/client/pages/settings/SettingsPage.tsx +28 -0
  31. package/src/client/review-desk/ConversationReviewItems.tsx +99 -0
  32. package/src/client/review-desk/queries.ts +30 -6
  33. package/src/mock-reporting/routes.ts +1 -1
  34. package/src/tailwind.css +4 -0
  35. package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
  36. package/src/client/review-desk/ConversationReviewBar.tsx +0 -67
@@ -3,14 +3,13 @@ import {
3
3
  Archive,
4
4
  ArchiveRestore,
5
5
  Info,
6
- PanelRight,
7
6
  MessagesSquare,
8
7
  ScrollText,
9
8
  Search,
10
9
  X,
11
10
  } from "lucide-react";
12
11
 
13
- import { Button } from "../components/ui/button";
12
+ import { Button, buttonVariants } from "../components/ui/button";
14
13
  import {
15
14
  Tooltip,
16
15
  TooltipContent,
@@ -29,59 +28,74 @@ export type ConversationArchiveAction = {
29
28
  pending: boolean;
30
29
  };
31
30
 
32
- /** Show conversation actions in the header or mobile menu. */
31
+ /**
32
+ * Show conversation actions in the `⋯` menu. On desktop the title bar shows
33
+ * search, share, and details, so the menu leaves them out.
34
+ */
33
35
  export function ConversationHeaderActions(props: {
34
36
  archive: ConversationArchiveAction;
35
37
  conversationId: string;
36
38
  copyAction?: ReactNode;
37
39
  detailsOpen: boolean;
38
- detailsButtonRef?: Ref<HTMLButtonElement>;
39
- /** `bar` is the desktop icon row. `menu` stacks actions for mobile overflow. */
40
- layout?: "bar" | "menu";
41
40
  onDetailsClick(): void;
42
41
  onSearchClick(): void;
43
42
  onViewChange(value: TranscriptViewMode): void;
43
+ placement: "desktop" | "mobile";
44
44
  searchOpen: boolean;
45
+ sentryConversationUrl?: string;
45
46
  view: TranscriptViewMode;
46
47
  }) {
47
- const layout = props.layout ?? "bar";
48
- if (layout === "menu") {
49
- return (
50
- <div className="grid gap-0.5">
51
- {props.view === "rich" ? (
52
- <MenuActionButton
53
- label={props.searchOpen ? "Hide search" : "Search transcript"}
54
- onClick={props.onSearchClick}
55
- pressed={props.searchOpen}
56
- >
57
- {props.searchOpen ? (
58
- <X aria-hidden="true" size={16} strokeWidth={2} />
59
- ) : (
60
- <Search aria-hidden="true" size={16} strokeWidth={2} />
61
- )}
62
- </MenuActionButton>
63
- ) : null}
64
- <MenuActionButton
65
- label="Conversation"
66
- onClick={() => props.onViewChange("rich")}
67
- pressed={props.view === "rich"}
68
- >
69
- <MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
70
- </MenuActionButton>
48
+ const mobile = props.placement === "mobile";
49
+ return (
50
+ <div className="grid gap-0.5">
51
+ {mobile && props.view === "rich" ? (
71
52
  <MenuActionButton
72
- label="Event log"
73
- onClick={() => props.onViewChange("raw")}
74
- pressed={props.view === "raw"}
53
+ label={props.searchOpen ? "Hide search" : "Search transcript"}
54
+ onClick={props.onSearchClick}
55
+ pressed={props.searchOpen}
75
56
  >
76
- <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
57
+ {props.searchOpen ? (
58
+ <X aria-hidden="true" size={16} strokeWidth={2} />
59
+ ) : (
60
+ <Search aria-hidden="true" size={16} strokeWidth={2} />
61
+ )}
77
62
  </MenuActionButton>
63
+ ) : null}
64
+ <MenuActionButton
65
+ label="Conversation"
66
+ onClick={() => props.onViewChange("rich")}
67
+ pressed={props.view === "rich"}
68
+ >
69
+ <MessagesSquare aria-hidden="true" size={16} strokeWidth={2} />
70
+ </MenuActionButton>
71
+ <MenuActionButton
72
+ label="Event log"
73
+ onClick={() => props.onViewChange("raw")}
74
+ pressed={props.view === "raw"}
75
+ >
76
+ <ScrollText aria-hidden="true" size={16} strokeWidth={2} />
77
+ </MenuActionButton>
78
+ {mobile ? (
78
79
  <ShareConversationButton
79
80
  key={props.conversationId}
80
81
  conversationId={props.conversationId}
81
82
  layout="menu"
82
83
  />
83
- {props.copyAction}
84
- <ArchiveConversationButton {...props.archive} layout="menu" />
84
+ ) : null}
85
+ {props.copyAction}
86
+ <ArchiveConversationButton {...props.archive} />
87
+ {props.sentryConversationUrl ? (
88
+ <a
89
+ className={buttonVariants({ size: "menu" })}
90
+ href={props.sentryConversationUrl}
91
+ rel="noopener noreferrer"
92
+ target="_blank"
93
+ >
94
+ <SentryGlyph />
95
+ Open in Sentry
96
+ </a>
97
+ ) : null}
98
+ {mobile ? (
85
99
  <MenuActionButton
86
100
  label="Conversation details"
87
101
  onClick={props.onDetailsClick}
@@ -89,57 +103,29 @@ export function ConversationHeaderActions(props: {
89
103
  >
90
104
  <Info aria-hidden="true" size={16} strokeWidth={2} />
91
105
  </MenuActionButton>
92
- </div>
93
- );
94
- }
95
-
96
- return (
97
- <div className="flex shrink-0 items-center gap-0.5">
98
- {props.view === "rich" ? (
99
- <TranscriptSearchToggle
100
- onClick={props.onSearchClick}
101
- open={props.searchOpen}
102
- />
103
106
  ) : null}
104
- {props.copyAction}
105
- <ArchiveConversationButton {...props.archive} layout="bar" />
106
- <Button
107
- ref={props.detailsButtonRef}
108
- aria-label="Conversation details"
109
- aria-pressed={props.detailsOpen}
110
- className="ml-2"
111
- onClick={props.onDetailsClick}
112
- >
113
- <PanelRight aria-hidden="true" size={15} strokeWidth={2} />
114
- Details
115
- </Button>
116
107
  </div>
117
108
  );
118
109
  }
119
110
 
120
111
  /** Switch between the conversation and event log. */
121
112
  export function TranscriptViewToggle(props: {
122
- labelled?: boolean;
123
113
  onChange(value: TranscriptViewMode): void;
124
114
  value: TranscriptViewMode;
125
115
  }) {
126
116
  return (
127
117
  <ToggleGroup
128
118
  aria-label="Transcript view"
129
- className={
130
- props.labelled
131
- ? "inline-flex gap-6"
132
- : "inline-flex gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
133
- }
119
+ className="inline-flex gap-0.5 rounded-md border border-dashboard-border bg-dashboard-overlay-soft p-0.5"
134
120
  onValueChange={([next]) => {
135
121
  if (next) props.onChange(next);
136
122
  }}
137
123
  value={[props.value]}
138
124
  >
139
- <ViewModeItem label="Conversation" labelled={props.labelled} value="rich">
125
+ <ViewModeItem label="Conversation" value="rich">
140
126
  <MessagesSquare aria-hidden="true" size={14} strokeWidth={2} />
141
127
  </ViewModeItem>
142
- <ViewModeItem label="Event log" labelled={props.labelled} value="raw">
128
+ <ViewModeItem label="Event log" value="raw">
143
129
  <ScrollText aria-hidden="true" size={14} strokeWidth={2} />
144
130
  </ViewModeItem>
145
131
  </ToggleGroup>
@@ -149,28 +135,21 @@ export function TranscriptViewToggle(props: {
149
135
  function ViewModeItem(props: {
150
136
  children: ReactNode;
151
137
  label: string;
152
- labelled?: boolean;
153
138
  value: TranscriptViewMode;
154
139
  }) {
155
- const item = (
156
- <ToggleGroupItem
157
- aria-label={props.label}
158
- className={
159
- props.labelled
160
- ? "relative h-11 min-w-0 gap-2 rounded-none border-0 px-0 text-sm hover:bg-transparent after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 data-pressed:border-0 data-pressed:bg-transparent data-pressed:after:bg-dashboard-text-solid"
161
- : "size-7 min-w-0 border-0 px-0 data-pressed:border-0 data-pressed:bg-dashboard-fill-hover"
162
- }
163
- value={props.value}
164
- >
165
- {props.children}
166
- {props.labelled ? props.label : null}
167
- </ToggleGroupItem>
168
- );
169
- return props.labelled ? (
170
- item
171
- ) : (
140
+ return (
172
141
  <Tooltip>
173
- <TooltipTrigger render={item} />
142
+ <TooltipTrigger
143
+ render={
144
+ <ToggleGroupItem
145
+ aria-label={props.label}
146
+ className="size-7 min-w-0 border-0 px-0 data-pressed:border-0 data-pressed:bg-dashboard-fill-hover"
147
+ value={props.value}
148
+ >
149
+ {props.children}
150
+ </ToggleGroupItem>
151
+ }
152
+ />
174
153
  <TooltipContent size="compact">{props.label}</TooltipContent>
175
154
  </Tooltip>
176
155
  );
@@ -183,9 +162,11 @@ export function HeaderIconButton(props: {
183
162
  label: string;
184
163
  onClick(): void;
185
164
  pressed?: boolean;
165
+ /** Hide the tooltip, such as while the menu this button opens covers it. */
166
+ tooltipDisabled?: boolean;
186
167
  }) {
187
168
  return (
188
- <Tooltip>
169
+ <Tooltip disabled={props.tooltipDisabled}>
189
170
  <TooltipTrigger
190
171
  render={
191
172
  <Button
@@ -248,45 +229,36 @@ function MenuActionButton(props: {
248
229
  );
249
230
  }
250
231
 
251
- function ArchiveConversationButton(
252
- props: ConversationArchiveAction & { layout: "bar" | "menu" },
253
- ) {
232
+ function ArchiveConversationButton(props: ConversationArchiveAction) {
254
233
  const label = props.pending
255
234
  ? "Saving archive state"
256
235
  : props.archived
257
236
  ? "Unarchive"
258
237
  : "Archive";
259
238
  const Icon = props.archived ? ArchiveRestore : Archive;
260
- if (props.layout === "menu") {
261
- return (
262
- <Button
263
- aria-label={label}
264
- disabled={props.disabled}
265
- onClick={props.onClick}
266
- size="menu"
267
- >
268
- <Icon aria-hidden="true" size={16} strokeWidth={2} />
269
- <span>{label}</span>
270
- </Button>
271
- );
272
- }
273
239
  return (
274
- <Tooltip>
275
- <TooltipTrigger
276
- render={
277
- <Button
278
- focusableWhenDisabled
279
- aria-label={label}
280
- className="hidden shrink-0 text-dashboard-text-muted md:grid"
281
- disabled={props.disabled}
282
- onClick={props.onClick}
283
- size="icon"
284
- >
285
- <Icon aria-hidden="true" size={15} strokeWidth={2} />
286
- </Button>
287
- }
288
- />
289
- <TooltipContent size="compact">{label}</TooltipContent>
290
- </Tooltip>
240
+ <Button
241
+ aria-label={label}
242
+ disabled={props.disabled}
243
+ onClick={props.onClick}
244
+ size="menu"
245
+ >
246
+ <Icon aria-hidden="true" size={16} strokeWidth={2} />
247
+ <span>{label}</span>
248
+ </Button>
249
+ );
250
+ }
251
+
252
+ /** Draw the Sentry glyph in the current text color, sized like a menu icon. */
253
+ function SentryGlyph() {
254
+ return (
255
+ <svg
256
+ aria-hidden="true"
257
+ className="size-4 shrink-0"
258
+ fill="currentColor"
259
+ viewBox="0 0 24 24"
260
+ >
261
+ <path d="M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z" />
262
+ </svg>
291
263
  );
292
264
  }
@@ -10,7 +10,7 @@ import {
10
10
  LockKeyhole,
11
11
  TriangleAlert,
12
12
  } from "lucide-react";
13
- import { useSyncExternalStore } from "react";
13
+ import { useSyncExternalStore, type ReactNode } from "react";
14
14
  import { Link } from "react-router";
15
15
  import type { ConversationDetailReport } from "@sentry/junior/api/schema";
16
16
 
@@ -30,7 +30,6 @@ import {
30
30
  TooltipTitle,
31
31
  TooltipTrigger,
32
32
  } from "../components/ui/tooltip";
33
- import { MetricList, type MetricListItem } from "../components/Metric";
34
33
  import { cn, dashboardInlineLinkClass } from "../styles";
35
34
  import {
36
35
  CostMetric,
@@ -507,52 +506,64 @@ export function liveModelId(
507
506
  return openTurns.size > 0 ? modelId : undefined;
508
507
  }
509
508
 
510
- /** True when runtime stats have content for the requested presentation. */
509
+ /** True when runtime stats have content to show in details. */
511
510
  export function hasConversationStats(props: {
512
511
  conversation: Conversation | undefined;
513
512
  detail?: ConversationDetailReport;
514
- variant?: "compact" | "full";
515
513
  }): boolean {
516
514
  return conversationStatItems(props).length > 0;
517
515
  }
518
516
 
519
- type ConversationStatItem = MetricListItem & { label: string };
517
+ type ConversationStatItem = {
518
+ content: ReactNode;
519
+ key: string;
520
+ label: string;
521
+ /** Shown below `md` only, where the title bar is hidden. */
522
+ mobileOnly?: boolean;
523
+ };
520
524
 
521
- /**
522
- * Render runtime, source, token, and cost metadata. The compact variant is one
523
- * line under the conversation title. The full variant is a labeled list for
524
- * conversation details.
525
- */
525
+ /** Render runtime, source, token, and cost metadata as a labeled list. */
526
526
  export function ConversationStats(props: {
527
527
  conversation: Conversation | undefined;
528
528
  detail?: ConversationDetailReport;
529
- variant?: "compact" | "full";
530
529
  }) {
531
530
  const stats = conversationStatItems(props);
532
531
  if (stats.length === 0) return null;
532
+ return (
533
+ <dl className="m-0 grid min-w-0 gap-2 text-xs leading-snug">
534
+ {stats.map((item) => (
535
+ <div
536
+ className={cn(
537
+ "flex min-w-0 items-baseline justify-between gap-4",
538
+ item.mobileOnly && "md:hidden",
539
+ )}
540
+ key={item.key}
541
+ >
542
+ <dt className="shrink-0 text-dashboard-text-muted">{item.label}</dt>
543
+ <dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
544
+ {item.content}
545
+ </dd>
546
+ </div>
547
+ ))}
548
+ </dl>
549
+ );
550
+ }
533
551
 
534
- if ((props.variant ?? "full") === "full") {
535
- return (
536
- <dl className="m-0 grid min-w-0 gap-2 text-xs leading-snug">
537
- {stats.map((item) => (
538
- <div
539
- className="flex min-w-0 items-baseline justify-between gap-4"
540
- key={item.key}
541
- >
542
- <dt className="shrink-0 text-dashboard-text-muted">{item.label}</dt>
543
- <dd className="m-0 min-w-0 break-words text-right tabular-nums text-dashboard-text">
544
- {item.content}
545
- </dd>
546
- </div>
547
- ))}
548
- </dl>
549
- );
550
- }
551
-
552
+ /**
553
+ * Name where a conversation runs, linked when the source has a URL. Returns
554
+ * null without a location, so callers can drop the slot around it.
555
+ */
556
+ export function conversationLocation(
557
+ conversation: Conversation | undefined,
558
+ detail: ConversationDetailReport | undefined,
559
+ ): ReactNode {
560
+ if (!conversation) return null;
561
+ const location = slackLocationLabel(conversation, { includeId: false });
562
+ if (!location) return null;
552
563
  return (
553
- <MetricList
554
- className="break-words text-xs leading-[1.45] text-dashboard-text-muted"
555
- items={stats}
564
+ <LocationLink
565
+ label={location}
566
+ locationUrl={detail?.locationUrl ?? conversation.locationUrl}
556
567
  />
557
568
  );
558
569
  }
@@ -560,10 +571,8 @@ export function ConversationStats(props: {
560
571
  function conversationStatItems(props: {
561
572
  conversation: Conversation | undefined;
562
573
  detail?: ConversationDetailReport;
563
- variant?: "compact" | "full";
564
574
  }): ConversationStatItem[] {
565
575
  if (!props.conversation) return [];
566
- const variant = props.variant ?? "full";
567
576
  const completeDetail = props.detail?.previousCursor
568
577
  ? undefined
569
578
  : props.detail;
@@ -572,11 +581,6 @@ function conversationStatItems(props: {
572
581
  const tokenSummary =
573
582
  summarizeModelUsage(props.detail?.modelUsage) ?? summarizeUsage(usage);
574
583
  const costSummary = summarizeCost(usage);
575
- const location = slackLocationLabel(props.conversation, {
576
- includeId: false,
577
- });
578
- const locationUrl =
579
- props.detail?.locationUrl ?? props.conversation.locationUrl;
580
584
  const durationLabel = formatConversationDuration(props.conversation);
581
585
  const live = (props.detail?.status ?? props.conversation.status) === "active";
582
586
  const activeModelId = liveModelId(props.detail);
@@ -584,22 +588,25 @@ function conversationStatItems(props: {
584
588
  const childAgentTotals =
585
589
  props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
586
590
  const lastActivityAt = props.conversation.lastSeenAt;
591
+ const location = conversationLocation(props.conversation, props.detail);
587
592
  const rawStats: Array<ConversationStatItem | undefined> = [
593
+ // The desktop title bar shows the location. Mobile hides that bar.
588
594
  location
589
595
  ? {
590
- content: <LocationLink label={location} locationUrl={locationUrl} />,
596
+ content: location,
591
597
  key: "location",
592
598
  label: "Location",
599
+ mobileOnly: true,
593
600
  }
594
601
  : undefined,
595
- variant === "full" && sourceTask
602
+ sourceTask
596
603
  ? {
597
604
  content: <SourceTask sourceTask={sourceTask} />,
598
605
  key: "source-task",
599
606
  label: "Source",
600
607
  }
601
608
  : undefined,
602
- variant === "full" && lastActivityAt
609
+ lastActivityAt
603
610
  ? {
604
611
  content: (
605
612
  <time
@@ -635,7 +642,6 @@ function conversationStatItems(props: {
635
642
  ? {
636
643
  content: (
637
644
  <TokenMetric
638
- detailsHint={variant === "compact"}
639
645
  childAgentTotals={childAgentTotals}
640
646
  compactionCount={
641
647
  completeDetail
@@ -661,7 +667,6 @@ function conversationStatItems(props: {
661
667
  ? {
662
668
  content: (
663
669
  <CostMetric
664
- detailsHint={variant === "compact"}
665
670
  auxiliaryCosts={
666
671
  props.detail?.auxiliaryCosts ??
667
672
  props.conversation.auxiliaryCosts
@@ -0,0 +1,59 @@
1
+ import { Ellipsis } from "lucide-react";
2
+ import { useEffect, useRef, useState, type ReactNode, type Ref } from "react";
3
+
4
+ import { HeaderIconButton } from "./ConversationHeaderActions";
5
+
6
+ /**
7
+ * Show the conversation `⋯` button and its menu. The menu closes on an
8
+ * outside click or Escape. `children` gets `close` so an item can close the
9
+ * menu before it acts.
10
+ */
11
+ export function ConversationOverflowMenu(props: {
12
+ buttonRef?: Ref<HTMLButtonElement>;
13
+ children(close: () => void): ReactNode;
14
+ }) {
15
+ const [open, setOpen] = useState(false);
16
+ const menuRef = useRef<HTMLDivElement>(null);
17
+
18
+ useEffect(() => {
19
+ if (!open) return;
20
+
21
+ function closeOnOutsideClick(event: PointerEvent) {
22
+ if (!menuRef.current?.contains(event.target as Node)) setOpen(false);
23
+ }
24
+
25
+ // Capture runs before the details panel's document listener, and
26
+ // preventDefault tells it that this Escape is taken.
27
+ function closeOnEscape(event: KeyboardEvent) {
28
+ if (event.key !== "Escape" || event.defaultPrevented) return;
29
+ event.preventDefault();
30
+ setOpen(false);
31
+ }
32
+
33
+ document.addEventListener("pointerdown", closeOnOutsideClick);
34
+ document.addEventListener("keydown", closeOnEscape, true);
35
+ return () => {
36
+ document.removeEventListener("pointerdown", closeOnOutsideClick);
37
+ document.removeEventListener("keydown", closeOnEscape, true);
38
+ };
39
+ }, [open]);
40
+
41
+ return (
42
+ <div className="relative" ref={menuRef}>
43
+ <HeaderIconButton
44
+ ref={props.buttonRef}
45
+ label={open ? "Close conversation menu" : "Conversation menu"}
46
+ onClick={() => setOpen((value) => !value)}
47
+ pressed={open}
48
+ tooltipDisabled={open}
49
+ >
50
+ <Ellipsis aria-hidden="true" size={18} strokeWidth={2} />
51
+ </HeaderIconButton>
52
+ {open ? (
53
+ <div className="absolute right-0 top-[calc(100%+0.35rem)] z-40 w-56 rounded-xl border border-dashboard-border-strong bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl">
54
+ {props.children(() => setOpen(false))}
55
+ </div>
56
+ ) : null}
57
+ </div>
58
+ );
59
+ }