@sentry/junior-dashboard 0.263.0 → 0.265.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 (197) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +2 -5
  3. package/dist/app.js +119 -35
  4. package/dist/client.js +148 -135
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +119 -35
  7. package/dist/tailwind.css +1 -1
  8. package/package.json +9 -4
  9. package/src/api/schema.ts +0 -1
  10. package/src/app.ts +5 -20
  11. package/src/client/App.tsx +29 -31
  12. package/src/client/api.ts +7 -0
  13. package/src/client/components/BottomSheet.tsx +26 -18
  14. package/src/client/components/CopyButton.tsx +83 -0
  15. package/src/client/components/DetailsPanel.tsx +133 -0
  16. package/src/client/components/Drawer.tsx +34 -33
  17. package/src/client/components/FilterBar.tsx +16 -31
  18. package/src/client/components/Metric.tsx +33 -23
  19. package/src/client/components/PageContentSkeleton.tsx +1 -1
  20. package/src/client/components/Pagination.tsx +1 -1
  21. package/src/client/components/ParticipantAvatarStack.tsx +40 -36
  22. package/src/client/components/ProfileMenu.tsx +27 -93
  23. package/src/client/components/README.md +101 -64
  24. package/src/client/components/SegmentedNav.tsx +1 -1
  25. package/src/client/components/ShimmerText.tsx +2 -6
  26. package/src/client/components/ThemeSwitcher.tsx +19 -15
  27. package/src/client/components/charts/ActivityChart.tsx +173 -288
  28. package/src/client/components/charts/CacheWrites.tsx +63 -50
  29. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  30. package/src/client/components/charts/README.md +4 -4
  31. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  32. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  33. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  34. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  35. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  36. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  37. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  38. package/src/client/components/ui/accordion.tsx +77 -0
  39. package/src/client/components/ui/alert-dialog.tsx +89 -0
  40. package/src/client/components/ui/button.tsx +85 -0
  41. package/src/client/components/ui/chart.tsx +74 -0
  42. package/src/client/components/ui/checkbox.tsx +26 -0
  43. package/src/client/components/ui/collapsible.tsx +22 -0
  44. package/src/client/components/ui/field.tsx +49 -0
  45. package/src/client/components/ui/input.tsx +35 -0
  46. package/src/client/components/ui/native-select.tsx +57 -0
  47. package/src/client/components/ui/popover.tsx +67 -0
  48. package/src/client/components/ui/radio-group.tsx +32 -0
  49. package/src/client/components/ui/sheet.tsx +151 -0
  50. package/src/client/components/ui/skeleton.tsx +19 -0
  51. package/src/client/components/ui/table.tsx +80 -0
  52. package/src/client/components/ui/tabs.tsx +75 -0
  53. package/src/client/components/ui/textarea.tsx +17 -0
  54. package/src/client/components/ui/toast.tsx +215 -0
  55. package/src/client/components/ui/toggle-group.tsx +36 -0
  56. package/src/client/components/ui/toggle.tsx +37 -0
  57. package/src/client/components/ui/tooltip.tsx +168 -0
  58. package/src/client/components/ui/trace.tsx +96 -0
  59. package/src/client/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
3
3
  import { cn } from "../styles";
4
4
  import { Card } from "./layout/Card";
5
5
  import { SearchInput } from "./SearchInput";
6
+ import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
6
7
 
7
8
  /** Lay out labeled filter groups and an optional search field. */
8
9
  export function FilterBar(props: {
@@ -69,43 +70,27 @@ export function FilterTabList(props: {
69
70
  value: string;
70
71
  }) {
71
72
  return (
72
- <div
73
- aria-label={props.ariaLabel}
74
- className="grid min-w-0 grid-cols-3 gap-1 border-b border-dashboard-border-subtle sm:flex sm:overflow-x-auto"
75
- role="tablist"
76
- >
77
- {props.items.map((item) => {
78
- const selected = props.value === item.value;
79
- return (
80
- <button
81
- aria-selected={selected}
82
- className={cn(
83
- "relative flex min-w-0 cursor-pointer items-center justify-between gap-2 border-0 bg-transparent px-3 py-2.5 font-sans font-medium text-xs transition-colors after:absolute after:inset-x-2 after:bottom-0 after:h-px sm:shrink-0 sm:justify-start",
84
- selected
85
- ? "text-dashboard-accent-text after:bg-dashboard-accent"
86
- : "text-dashboard-text-muted after:bg-transparent hover:text-dashboard-text",
87
- )}
73
+ <Tabs onValueChange={props.onChange} value={props.value}>
74
+ <TabsList
75
+ aria-label={props.ariaLabel}
76
+ className="grid grid-cols-3 sm:flex sm:overflow-x-auto"
77
+ variant="line"
78
+ >
79
+ {props.items.map((item) => (
80
+ <TabsTrigger
81
+ className="group/tab justify-between sm:shrink-0 sm:justify-start"
88
82
  key={item.value || "all"}
89
- onClick={() => props.onChange(item.value)}
90
- role="tab"
91
- type="button"
83
+ value={item.value}
92
84
  >
93
85
  {item.label}
94
86
  {item.count !== undefined ? (
95
- <span
96
- className={cn(
97
- "rounded-sm border px-1.5 py-0.5 text-xs",
98
- selected
99
- ? "border-dashboard-accent/20 bg-dashboard-accent/10 text-dashboard-accent-text"
100
- : "border-dashboard-border bg-dashboard-fill-soft text-dashboard-text-muted",
101
- )}
102
- >
87
+ <span className="rounded-sm border border-dashboard-border bg-dashboard-fill-soft px-1.5 py-0.5 text-xs text-dashboard-text-muted group-data-active/tab:border-dashboard-accent/20 group-data-active/tab:bg-dashboard-accent/10 group-data-active/tab:text-dashboard-accent-text">
103
88
  {item.count.toLocaleString("en-US")}
104
89
  </span>
105
90
  ) : null}
106
- </button>
107
- );
108
- })}
109
- </div>
91
+ </TabsTrigger>
92
+ ))}
93
+ </TabsList>
94
+ </Tabs>
110
95
  );
111
96
  }
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import { cn } from "../styles";
4
4
  import { ShimmerText } from "./ShimmerText";
5
- import { Tooltip } from "./Tooltip";
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
6
6
 
7
7
  export type MetricTooltipLine = {
8
8
  label?: string;
@@ -85,34 +85,44 @@ export function MetricValue(props: {
85
85
  }
86
86
 
87
87
  return (
88
- <Tooltip
89
- align={props.align}
90
- className={cn(
91
- "w-[calc(100vw-2rem)] max-w-80 rounded-lg border border-dashboard-border-emphasis bg-dashboard-bg text-left text-xs font-normal text-dashboard-text-muted shadow-xl shadow-black/35",
92
- props.tooltipCard ? "p-3 leading-snug" : "px-3 py-2 leading-relaxed",
93
- )}
94
- content={
95
- props.tooltipCard ? (
96
- <span className="block max-h-[min(30rem,var(--radix-hover-card-content-available-height,30rem))] overflow-y-auto">
88
+ <Tooltip>
89
+ <TooltipTrigger
90
+ render={
91
+ <span
92
+ className={cn(
93
+ "inline-flex border-b border-dotted border-dashboard-border-interactive outline-none transition-colors hover:border-dashboard-border-heavy focus-visible:border-dashboard-border-heavy",
94
+ props.className,
95
+ )}
96
+ tabIndex={0}
97
+ >
98
+ {props.children}
99
+ </span>
100
+ }
101
+ />
102
+ <TooltipContent
103
+ align={
104
+ props.align === "left"
105
+ ? "start"
106
+ : props.align === "right"
107
+ ? "end"
108
+ : "center"
109
+ }
110
+ className={cn(
111
+ "w-[calc(100vw-2rem)] max-w-80 rounded-lg bg-dashboard-bg text-left font-normal shadow-xl shadow-black/35",
112
+ props.tooltipCard ? "p-3 leading-snug" : "px-3 py-2 leading-relaxed",
113
+ )}
114
+ side={props.tooltipPlacement === "above" ? "top" : "bottom"}
115
+ >
116
+ {props.tooltipCard ? (
117
+ <span className="block max-h-[min(30rem,var(--available-height,30rem))] overflow-y-auto">
97
118
  {props.tooltipCard}
98
119
  </span>
99
120
  ) : tooltip ? (
100
121
  <span className="block max-h-72 overflow-y-auto">
101
122
  <TooltipLines lines={tooltip} />
102
123
  </span>
103
- ) : null
104
- }
105
- placement={props.tooltipPlacement ?? "below"}
106
- >
107
- <span
108
- className={cn(
109
- "inline-flex border-b border-dotted border-dashboard-border-interactive outline-none transition-colors hover:border-dashboard-border-heavy focus-visible:border-dashboard-border-heavy",
110
- props.className,
111
- )}
112
- tabIndex={0}
113
- >
114
- {props.children}
115
- </span>
124
+ ) : null}
125
+ </TooltipContent>
116
126
  </Tooltip>
117
127
  );
118
128
  }
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
3
  import { Card } from "./layout/Card";
4
- import { Skeleton } from "./Skeleton";
4
+ import { Skeleton } from "./ui/skeleton";
5
5
  import { cn } from "../styles";
6
6
 
7
7
  export type PageContentSkeletonVariant =
@@ -1,7 +1,7 @@
1
1
  import { ChevronLeft, ChevronRight } from "lucide-react";
2
2
 
3
3
  import { cn } from "../styles";
4
- import { Button } from "./Button";
4
+ import { Button } from "./ui/button";
5
5
 
6
6
  /** Render a centered load-more control for cursor or progressive lists. */
7
7
  export function LoadMorePagination(props: {
@@ -4,7 +4,7 @@ import { actorLabel } from "../format";
4
4
  import { cn } from "../styles";
5
5
  import type { Conversation } from "../types";
6
6
  import { ActorAvatar } from "./ActorAvatar";
7
- import { Tooltip } from "./Tooltip";
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
8
8
 
9
9
  const MAX_VISIBLE_PARTICIPANTS = 3;
10
10
 
@@ -57,22 +57,25 @@ function Avatar(props: {
57
57
  }) {
58
58
  const label = participantDescription(props.participant);
59
59
  return (
60
- <Tooltip
61
- content={<ParticipantTooltipContent participant={props.participant} />}
62
- focusable
63
- triggerClassName="-ml-1 first:ml-0 pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
64
- >
65
- <span
66
- aria-label={label}
67
- className="relative inline-flex rounded-full focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
68
- onKeyDown={(event) => event.stopPropagation()}
69
- tabIndex={0}
70
- >
71
- <ActorAvatar
72
- name={participantName(props.participant)}
73
- size={props.size}
74
- />
75
- </span>
60
+ <Tooltip>
61
+ <TooltipTrigger
62
+ render={
63
+ <span
64
+ aria-label={label}
65
+ className="relative -ml-1 inline-flex rounded-full first:ml-0 pointer-events-none focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
66
+ onKeyDown={(event) => event.stopPropagation()}
67
+ tabIndex={0}
68
+ >
69
+ <ActorAvatar
70
+ name={participantName(props.participant)}
71
+ size={props.size}
72
+ />
73
+ </span>
74
+ }
75
+ />
76
+ <TooltipContent>
77
+ <ParticipantTooltipContent participant={props.participant} />
78
+ </TooltipContent>
76
79
  </Tooltip>
77
80
  );
78
81
  }
@@ -105,8 +108,25 @@ export function ParticipantAvatarStack(props: ParticipantAvatarStackProps) {
105
108
  />
106
109
  ))}
107
110
  {hidden.length > 0 ? (
108
- <Tooltip
109
- content={
111
+ <Tooltip>
112
+ <TooltipTrigger
113
+ render={
114
+ <span
115
+ aria-label={`${hidden.length} more participants: ${hidden
116
+ .map(participantDescription)
117
+ .join("; ")}`}
118
+ className={cn(
119
+ "relative -ml-1 inline-grid shrink-0 pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto place-items-center rounded-full border-2 border-dashboard-bg bg-dashboard-surface-active font-sans font-semibold tabular-nums leading-none text-dashboard-text-muted focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
120
+ props.size === "list" ? "size-6 text-2xs" : "size-7 text-xs",
121
+ )}
122
+ onKeyDown={(event) => event.stopPropagation()}
123
+ tabIndex={0}
124
+ >
125
+ +{hidden.length}
126
+ </span>
127
+ }
128
+ />
129
+ <TooltipContent>
110
130
  <span className="grid gap-1 font-sans">
111
131
  {hidden.map((participant, index) => (
112
132
  <span key={participantKey(participant, index)}>
@@ -114,23 +134,7 @@ export function ParticipantAvatarStack(props: ParticipantAvatarStackProps) {
114
134
  </span>
115
135
  ))}
116
136
  </span>
117
- }
118
- focusable
119
- triggerClassName="-ml-1 pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
120
- >
121
- <span
122
- aria-label={`${hidden.length} more participants: ${hidden
123
- .map(participantDescription)
124
- .join("; ")}`}
125
- className={cn(
126
- "relative inline-grid shrink-0 place-items-center rounded-full border-2 border-dashboard-bg bg-dashboard-surface-active font-sans font-semibold tabular-nums leading-none text-dashboard-text-muted focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
127
- props.size === "list" ? "size-6 text-2xs" : "size-7 text-xs",
128
- )}
129
- onKeyDown={(event) => event.stopPropagation()}
130
- tabIndex={0}
131
- >
132
- +{hidden.length}
133
- </span>
137
+ </TooltipContent>
134
138
  </Tooltip>
135
139
  ) : null}
136
140
  </span>
@@ -8,12 +8,7 @@ import {
8
8
  ShieldCheck,
9
9
  UserRound,
10
10
  } from "lucide-react";
11
- import {
12
- useEffect,
13
- useRef,
14
- useState,
15
- type PointerEvent as ReactPointerEvent,
16
- } from "react";
11
+ import { useState } from "react";
17
12
  import { Link } from "react-router";
18
13
  import type { PersonalSpendReport } from "@sentry/junior/api/schema";
19
14
  import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
@@ -23,6 +18,7 @@ import { pluginUserPagePath } from "../pages/user/PluginUserPage";
23
18
  import { cn, dashboardInteractiveTextClass } from "../styles";
24
19
  import type { Identity } from "../types";
25
20
  import { ThemeSwitcher } from "./ThemeSwitcher";
21
+ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
26
22
 
27
23
  type ProfileMenuProps = {
28
24
  identity: Identity;
@@ -65,14 +61,6 @@ export function ProfileMenu({
65
61
  variant = "popover",
66
62
  }: ProfileMenuProps) {
67
63
  const [open, setOpen] = useState(false);
68
- const rootRef = useRef<HTMLDivElement>(null);
69
- const triggerRef = useRef<HTMLButtonElement>(null);
70
- const openTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
71
- undefined,
72
- );
73
- const closeTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
74
- undefined,
75
- );
76
64
  const email = identity.user.email!;
77
65
  const name = identity.user.name?.trim() || email;
78
66
  const sevenDaySpend = spend
@@ -85,54 +73,6 @@ export function ProfileMenu({
85
73
  (page) => page.navigation === "profile",
86
74
  );
87
75
 
88
- function clearHoverTimers() {
89
- if (openTimerRef.current) clearTimeout(openTimerRef.current);
90
- if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
91
- openTimerRef.current = undefined;
92
- closeTimerRef.current = undefined;
93
- }
94
-
95
- function openOnHover(event: ReactPointerEvent<HTMLDivElement>) {
96
- if (event.pointerType !== "mouse") return;
97
- clearHoverTimers();
98
- openTimerRef.current = setTimeout(() => setOpen(true), HOVER_OPEN_DELAY_MS);
99
- }
100
-
101
- function closeOnHover(event: ReactPointerEvent<HTMLDivElement>) {
102
- if (event.pointerType !== "mouse") return;
103
- clearHoverTimers();
104
- closeTimerRef.current = setTimeout(
105
- () => setOpen(false),
106
- HOVER_CLOSE_DELAY_MS,
107
- );
108
- }
109
-
110
- useEffect(() => clearHoverTimers, []);
111
-
112
- useEffect(() => {
113
- if (variant !== "popover" || !open) return;
114
-
115
- function closeOnOutsideClick(event: PointerEvent) {
116
- if (!rootRef.current?.contains(event.target as Node)) {
117
- setOpen(false);
118
- }
119
- }
120
-
121
- function closeOnEscape(event: KeyboardEvent) {
122
- if (event.key === "Escape") {
123
- setOpen(false);
124
- triggerRef.current?.focus();
125
- }
126
- }
127
-
128
- document.addEventListener("pointerdown", closeOnOutsideClick);
129
- document.addEventListener("keydown", closeOnEscape);
130
- return () => {
131
- document.removeEventListener("pointerdown", closeOnOutsideClick);
132
- document.removeEventListener("keydown", closeOnEscape);
133
- };
134
- }, [open, variant]);
135
-
136
76
  const avatar = (
137
77
  <span
138
78
  aria-hidden="true"
@@ -335,32 +275,28 @@ export function ProfileMenu({
335
275
  }
336
276
 
337
277
  return (
338
- <div
339
- className="relative"
340
- onBlurCapture={(event) => {
341
- if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
342
- setOpen(false);
278
+ <Popover
279
+ onOpenChange={(nextOpen, details) => {
280
+ // A mouse click always keeps the menu open, also after hover opened it.
281
+ // A keyboard press (detail 0) still toggles it.
282
+ const mouseClick =
283
+ details.event instanceof MouseEvent && details.event.detail > 0;
284
+ if (!nextOpen && details.reason === "trigger-press" && mouseClick) {
285
+ return;
343
286
  }
287
+ setOpen(nextOpen);
344
288
  }}
345
- onPointerEnter={openOnHover}
346
- onPointerLeave={closeOnHover}
347
- ref={rootRef}
289
+ open={open}
348
290
  >
349
- <button
350
- aria-controls="profile-popover"
351
- aria-expanded={open}
352
- aria-haspopup="true"
291
+ <PopoverTrigger
353
292
  aria-label={`${open ? "Close" : "Open"} profile menu for ${name}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`}
354
293
  className={cn(
355
294
  "group flex h-10 cursor-pointer items-center gap-2 rounded-lg border-0 bg-transparent px-1.5 text-dashboard-text transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70",
356
295
  open && "bg-dashboard-fill-hover text-dashboard-text",
357
296
  )}
358
- onClick={(event) => {
359
- clearHoverTimers();
360
- setOpen(event.detail === 0 ? (value) => !value : true);
361
- }}
362
- ref={triggerRef}
363
- type="button"
297
+ closeDelay={HOVER_CLOSE_DELAY_MS}
298
+ delay={HOVER_OPEN_DELAY_MS}
299
+ openOnHover
364
300
  >
365
301
  <span className="hidden items-center gap-2 md:flex">
366
302
  <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
@@ -399,18 +335,16 @@ export function ProfileMenu({
399
335
  size={14}
400
336
  strokeWidth={2}
401
337
  />
402
- </button>
403
-
404
- {open ? (
405
- <div
406
- className="absolute right-0 top-[calc(100%+0.5rem)] z-40 w-64 rounded-xl bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl"
407
- id="profile-popover"
408
- >
409
- {identityBlock}
410
- <ThemeSwitcher className="border-b border-dashboard-border-strong px-2.5 py-2" />
411
- {menuLinks(() => setOpen(false))}
412
- </div>
413
- ) : null}
414
- </div>
338
+ </PopoverTrigger>
339
+ <PopoverContent
340
+ align="end"
341
+ className="w-64 rounded-xl border-0 bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl"
342
+ id="profile-popover"
343
+ >
344
+ {identityBlock}
345
+ <ThemeSwitcher className="border-b border-dashboard-border-strong px-2.5 py-2" />
346
+ {menuLinks(() => setOpen(false))}
347
+ </PopoverContent>
348
+ </Popover>
415
349
  );
416
350
  }
@@ -4,6 +4,74 @@ Shared dashboard components live here. Feature pages compose these surfaces
4
4
  instead of copying Tailwind blocks. See `policies/frontend-components.md` for
5
5
  the policy. `pnpm dashboard-style:check` enforces the token rules below.
6
6
 
7
+ ## shadcn/ui
8
+
9
+ `packages/junior-dashboard/components.json` configures shadcn/ui for this package.
10
+ It uses Base UI (`base-nova`), Tailwind 4, and Lucide. Add components from the
11
+ repository root:
12
+
13
+ ```sh
14
+ pnpm --filter @sentry/junior-dashboard exec shadcn add <component>
15
+ ```
16
+
17
+ The CLI writes components to `src/client/components/ui/`. Its `@dashboard/*`
18
+ alias points to `src/client/*`. The existing `@/*` alias still points to core.
19
+ The `ui/` files join classes with the merging `cn` from
20
+ `src/client/lib/utils.ts`, so a caller's `className` wins over a base class.
21
+
22
+ The shadcn color tokens in `src/tailwind.css` use the dashboard tokens in both
23
+ themes. The `dark:` variant follows `data-theme`. Keep the existing Tailwind
24
+ radius scale. Do not apply the CLI's global border or body resets. Check each
25
+ added component against the rules below before use.
26
+
27
+ Each `ui/` file is shadcn source that uses dashboard tokens. Remove exports
28
+ that no app code uses. Use `ui/` components directly. Do not add an app
29
+ component that only renames a `ui/` component or its props. Change the `ui/`
30
+ file when every caller needs the same style or variant. App components compose
31
+ `ui/` source only when they add structure or behavior:
32
+
33
+ | App component | shadcn source |
34
+ | ------------------------------ | ----------------- |
35
+ | `Drawer`, `BottomSheet` | `ui/sheet` |
36
+ | `layout/DashboardHeader` (nav) | `ui/sheet` |
37
+ | `ProfileMenu` (desktop) | `ui/popover` |
38
+ | `TimeRangeSelector` | `ui/toggle-group` |
39
+ | `ThemeSwitcher` | `ui/toggle-group` |
40
+ | `TranscriptViewToggle` | `ui/toggle-group` |
41
+ | `FilterTabList` | `ui/tabs` |
42
+ | `charts/ActivityChartTooltip` | `ui/tooltip` |
43
+ | `conversations/ToolFrame` | `ui/trace` |
44
+ | `TranscriptReasoningView` | `ui/trace` |
45
+ | Recharts activity charts | `ui/chart` |
46
+
47
+ Rules for adopted components:
48
+
49
+ - Use `ui/trace` for transcript activity rows. It composes `ui/collapsible`.
50
+ A closed tool row unmounts its panel, so it does not run Shiki. Reasoning
51
+ sets `hiddenUntilFound`, so browser find reaches closed text. Transcript
52
+ search holds rows open and turns off the row button. Put only inline
53
+ elements, such as `span`, inside the row button.
54
+ - Use `ui/alert-dialog` for every confirmation. Do not use `window.confirm`.
55
+ Keep the confirmed item in state after close, so the closing dialog keeps its
56
+ text.
57
+ - `ui/toast` has no global notification manager. Features call
58
+ `useToastManager` inside the mounted `Toaster` provider.
59
+ - Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
60
+ `aria-label`.
61
+ - Give an icon-only control its label with `TooltipContent size="compact"`.
62
+ Do not use the native `title` attribute. Tooltips open on mouse hover and
63
+ keyboard focus only, so a tap still runs the control.
64
+ - When a `ui/button` tooltip trigger can be disabled, set
65
+ `focusableWhenDisabled`. A native disabled button can drop hover and focus,
66
+ so Firefox closes the tooltip and hides pending or result labels.
67
+ `ui/button` styles `data-disabled`, so both disabled modes look the same.
68
+ - Build a form field from `ui/field`. Give `FieldError` the id
69
+ `<control id>-error` and `FieldDescription` the id `<control id>-help`. The
70
+ control sets `aria-invalid` and `aria-describedby`.
71
+ - For a link that looks like a button, put `buttonVariants()` on the router
72
+ `Link`. A form page has one `Button variant="primary"` for its main save
73
+ action.
74
+
7
75
  ## Foundations
8
76
 
9
77
  Tokens live in the `@theme` block of `src/tailwind.css`. Keep every color in a
@@ -16,9 +84,9 @@ token so each theme can redefine it.
16
84
  paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
17
85
  the choice for this browser. Use the `light:` variant only for one-off values
18
86
  that no token covers, such as provider brand colors.
19
- - **Charts.** Use `var(--color-*)` for series colors in SVG attributes, not hex
20
- values, so they follow the theme.
21
-
87
+ - **Charts.** Use `var(--color-*)` for series colors, not hex values, so they
88
+ follow the theme. Name series colors in the `ChartContainer` config and fill
89
+ each series with `var(--color-<key>)`.
22
90
  - **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
23
91
  `bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
24
92
  `surface-raised` (buttons, tooltips), `control`, `surface-hover`,
@@ -51,82 +119,53 @@ token so each theme can redefine it.
51
119
  use `rounded-lg`. The composer, menus, and transcript object cards use
52
120
  `rounded-xl`. Avatars and count badges use `rounded-full`.
53
121
 
54
- ## Shared class helpers
55
-
56
- `src/client/styles.ts` holds the few class strings that many components share.
122
+ Shared class strings live in `src/client/styles.ts`.
57
123
 
58
- - `cn` joins class names.
59
- - `dashboardContainerClass` sets the page width.
60
- - `dashboardInlineLinkClass` styles a neutral underlined link inside prose.
61
- - `dashboardInteractiveTextClass` styles muted text that brightens on hover.
62
- - `dashboardComposerSurfaceClass` styles the composer surface.
124
+ ## App components
63
125
 
64
- ## Components
65
-
66
- ### Primitives
67
-
68
- - **Actions.** `Button` (`default`, `primary`, `danger`, and `size="icon"`) and
69
- `ButtonLink`. Form pages use `Button tone="primary"` for their one main save
70
- action. Use `ToggleButton` variants (`pill`, `segment`, `text`) for pressed
71
- states, not one-off styles.
72
- - **Forms.** `Field` (`default`, `compact`), `TextInput`, `TextArea`, `Select`,
73
- and `SearchInput`. Use `TextInput size="comfortable"` for larger mobile
74
- controls. Use `TextArea prose` for instructions, not code. `Field error`
75
- renders an error at `<htmlFor>-error`. The owning control must set
76
- `aria-invalid` and `aria-describedby`. `Select` owns the native control and its
77
- inset arrow. Use its `filter` and `sort` variants for compact controls.
78
126
  - **Status.** Use `StatusDot` for dense table or list markers. Use `StatusChip`
79
- when the label must stay visible. `StatusChip` is a small dot and
80
- sentence-case text, with no border or fill. It is not a control.
127
+ when the label must stay visible. `StatusChip` is not a control.
81
128
  - **Messages.** Use `Notice` for compact transient messages and `FormNotice`
82
129
  for wrapping form recovery text.
83
- - **Overlays.** `Sheet` is a local adaptation of shadcn/ui's Base UI Sheet.
84
- It uses Base UI Drawer for focus, dismissal, and native swipe support. The
85
- app's shared scroll lock keeps it in sync with the mobile conversation shell.
86
- `SheetContent` owns side placement, backdrop fade, and slide transitions.
87
- `Drawer` adds a visible header and close control. `BottomSheet` adds a swipe
88
- handle and keeps content scrolling separate from gestures. Both skip motion
89
- when reduced motion is set. Docked conversation details use a non-modal aside
90
- so the chat stays usable. The copied component's license is in `Sheet.LICENSE`.
91
- `Tooltip` and `IconButtonTooltip` own hover text.
92
- - **Identity.** `ActorAvatar`, `ParticipantAvatarStack`, and `ObjectIcon`.
93
- `ObjectIcon` owns native Ticket, Code change, Automation, Deployment, and
94
- Item icons. It uses the shared type and status mapping and the original
95
- Octicons paths. Do not replace it with a provider logo or a status-only icon.
96
- - **Loading.** Use `Skeleton` for placeholder leaves and `PageContentSkeleton`
130
+ - **Disclosure.** Use `ui/collapsible` for one show-or-hide section. Put a
131
+ `ChevronRight` with `dashboardDisclosureChevronClass` in a `group` trigger.
132
+ Set `hiddenUntilFound` on the content when browser find must reach closed
133
+ text. Closed content then keeps its box, so put margins and padding on a
134
+ child. Use `ui/accordion` for a set of sections with headings.
135
+ - **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
136
+ `BottomSheet` below that width. It owns Escape and the scroll frame for
137
+ docked panels.
138
+ - **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
139
+ Deployment, and Item icons. Do not replace it with a provider logo or a
140
+ status-only icon.
141
+ - **Loading.** Use `ui/skeleton` for placeholder leaves and `PageContentSkeleton`
97
142
  variants for page bodies. Use `PageRouteLoading` when a page keeps its real
98
143
  header above a body skeleton. Use a feature-owned loading component for
99
144
  distinct geometry, such as conversations.
100
145
 
101
- ### Navigation
146
+ ## Navigation
102
147
 
103
148
  The dashboard has three navigation levels. Each has its own look, so the
104
149
  levels stay distinct.
105
150
 
106
151
  - **App navigation** in `layout/DashboardHeader` uses pills.
107
152
  - **Page links** use `SegmentedNav`, through `layout/SecondaryNavigation`. They
108
- are underline tabs that sit on the header border. With `fill`, links share
109
- the row width while their labels fit. Rows that do not fit scroll sideways.
110
- - **Panel switches** use `SegmentedTabs`, which draws a pill track. Pass the
111
- selected value, items, change handler, and panel content. It owns tab and
112
- panel labels and keyboard focus. Pass `scroll` to keep the tabs fixed while
113
- the panel content scrolls. That mode fills its frame and owns the content
114
- padding, so its scrollbar stays at the frame edge. `BottomSheet` content owns
115
- its own padding.
153
+ are underline tabs that sit on the header border.
154
+ - **Panel switches** use `ui/tabs`. Give `TabsList` an `aria-label`. Set
155
+ `activateOnFocus` so arrow keys select the tab.
116
156
 
117
- Keep collection filters in `FilterTabList`.
157
+ Keep collection filters in `FilterTabList`. It uses `TabsList variant="line"`
158
+ and adds the count badges.
118
159
 
119
- ### Patterns
160
+ ## Patterns
120
161
 
121
162
  - **Layout.** `layout/` holds the shell chrome, page headers, cards, and
122
163
  section titles. `DashboardChrome` owns the sticky header block and its one
123
164
  bottom border.
124
- - **Directory tables and metrics.** `directory/` and `metrics/`.
125
- - **Charts.** Activity time-series charts own bucket labels in
126
- `charts/ActivityChart` (`formatActivityDate` for axes,
127
- `ActivityChartTooltip` and `formatActivityTooltipDate` for hover labels). New
128
- day or hour charts compose those helpers. They do not format UTC bucket keys
129
- inline.
165
+ - **Charts.** Put `role="img"` and the chart `aria-label` on the Recharts
166
+ chart, so the tooltip live region stays outside the image. Day and hour
167
+ charts compose the `charts/ActivityChart` axis, average line, and tooltip
168
+ helpers. They do not format UTC bucket keys inline.
130
169
  - **Conversation mobile geometry** is a hard ownership edge:
131
170
  - `layout/VisualViewportShell` owns the fixed shell, visualViewport CSS
132
171
  variables, and body lock.
@@ -138,16 +177,14 @@ Keep collection filters in `FilterTabList`.
138
177
  dock, and must not use `ComposerDock`. Do not add viewport height, offset, or
139
178
  bottom safe-area math in page modules.
140
179
 
141
- ### Features
142
-
143
180
  Feature surfaces stay under `conversations/` or `pages/` until two real callers
144
- share them. Transcript layout stays in `conversations/`. See
145
- [Transcript components](../conversations/README.md) for ownership.
181
+ share them. See [Transcript components](../conversations/README.md) for
182
+ transcript ownership.
146
183
 
147
184
  ## Rules
148
185
 
149
186
  - Prefer a named component on the second real use.
150
187
  - Keep Tailwind classes on the owning component.
151
- - Add gallery fixtures under `/dev/<section>` for new or changed shared
152
- components. Keep the `/dev` index as the catalog only.
188
+ - Check new or changed shared components on a real page with mock data and in
189
+ the dashboard E2E screenshots.
153
190
  - One-off page glue may stay inline when extraction only adds indirection.