@sentry/junior-dashboard 0.264.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 (191) 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 +145 -136
  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 +5 -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 +12 -2
  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 +15 -24
  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 +86 -77
  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 +13 -3
  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/conversations/ChatLayout.tsx +7 -3
  60. package/src/client/conversations/ConversationBrief.tsx +17 -6
  61. package/src/client/conversations/ConversationComposer.tsx +26 -12
  62. package/src/client/conversations/ConversationDetails.tsx +147 -62
  63. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  64. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  65. package/src/client/conversations/ConversationHeader.tsx +8 -3
  66. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  67. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  68. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  69. package/src/client/conversations/ConversationMeta.tsx +55 -88
  70. package/src/client/conversations/ConversationPage.tsx +220 -101
  71. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  72. package/src/client/conversations/ConversationWatches.tsx +165 -0
  73. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  74. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  75. package/src/client/conversations/DetailsList.tsx +124 -0
  76. package/src/client/conversations/EventDetails.tsx +42 -8
  77. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  78. package/src/client/conversations/ObjectCard.tsx +20 -8
  79. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  80. package/src/client/conversations/README.md +86 -13
  81. package/src/client/conversations/ShareConversationButton.tsx +25 -13
  82. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  83. package/src/client/conversations/ToolFrame.tsx +36 -52
  84. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  85. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  86. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  87. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  88. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  89. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  90. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  91. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  92. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  93. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  94. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  95. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  96. package/src/client/conversations/TranscriptView.tsx +63 -27
  97. package/src/client/conversations/eventLog.ts +57 -0
  98. package/src/client/conversations/eventTime.ts +60 -0
  99. package/src/client/conversations/queries.ts +29 -0
  100. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  101. package/src/client/format.ts +5 -0
  102. package/src/client/pages/ConnectionsPage.tsx +2 -2
  103. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  104. package/src/client/pages/SettingsPage.tsx +9 -10
  105. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  106. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  107. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  108. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  109. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  110. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  111. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  112. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  113. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  114. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  115. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  116. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  117. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  118. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  119. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  120. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  121. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  122. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  123. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  124. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  125. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  126. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  127. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  128. package/src/client/pages/automations/README.md +57 -0
  129. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  130. package/src/client/pages/automations/automationFormat.ts +37 -23
  131. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  132. package/src/client/pages/code/CodePage.tsx +52 -55
  133. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  134. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  135. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  136. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  137. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  138. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  139. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  140. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  141. package/src/client/pages/memory/memoryRecord.ts +1 -1
  142. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  143. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  144. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  145. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  146. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  147. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  148. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  149. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  150. package/src/client/pages/system/README.md +23 -0
  151. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  152. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  153. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  154. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  155. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  156. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  157. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  158. package/src/client/pages/system/sandboxSize.ts +8 -0
  159. package/src/client/pages/system/workspaceQueries.ts +53 -0
  160. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  161. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  162. package/src/client/styles.ts +4 -0
  163. package/src/client.tsx +7 -1
  164. package/src/mock-reporting/automation-collection.ts +4 -2
  165. package/src/mock-reporting/fixtures.ts +23 -7
  166. package/src/mock-reporting/routes.ts +12 -0
  167. package/src/mock-reporting/watches.ts +94 -0
  168. package/src/shell.ts +4 -9
  169. package/src/tailwind.css +0 -10
  170. package/src/client/components/Button.tsx +0 -118
  171. package/src/client/components/Field.tsx +0 -53
  172. package/src/client/components/SegmentedTabs.tsx +0 -119
  173. package/src/client/components/Select.tsx +0 -52
  174. package/src/client/components/Skeleton.tsx +0 -17
  175. package/src/client/components/TextInput.tsx +0 -58
  176. package/src/client/components/Tooltip.tsx +0 -216
  177. package/src/client/components/charts/useChartLayout.ts +0 -27
  178. package/src/client/components/ui/sheet.LICENSE +0 -21
  179. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  180. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  181. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  182. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  183. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  184. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  185. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  186. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  187. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  188. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  189. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  190. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  191. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -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
  }
@@ -16,23 +16,61 @@ pnpm --filter @sentry/junior-dashboard exec shadcn add <component>
16
16
 
17
17
  The CLI writes components to `src/client/components/ui/`. Its `@dashboard/*`
18
18
  alias points to `src/client/*`. The existing `@/*` alias still points to core.
19
- `src/client/lib/utils.ts` provides the shadcn `cn` helper. Existing components
20
- keep the class join helper in `src/client/styles.ts` until they are migrated.
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
21
 
22
22
  The shadcn color tokens in `src/tailwind.css` use the dashboard tokens in both
23
- themes. Primary actions use `text-solid` with inverse text. Danger uses the
24
- same theme-aware rose shade as existing danger controls. The `dark:` variant
25
- follows `data-theme`. Keep the existing Tailwind radius scale. Do not apply the CLI's global border or body resets to existing
26
- components. Check each added component against the rules below and add its
27
- gallery fixture before use.
28
-
29
- `ui/sheet.tsx` is the shadcn Base UI Dialog Sheet. It supports all four edges,
30
- uses the dashboard overlay and close button, and stops motion when reduced
31
- motion is set. The copied source license is in `ui/sheet.LICENSE`.
32
- `Drawer` and `BottomSheet` compose this component for app details.
33
- `ui/useSheet.ts` owns their close lifecycle, shared document scroll lock, and
34
- focus rules when a resize puts one modal below another. Conversation browser
35
- scenarios cover dismissal, nested focus, and layout changes.
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.
36
74
 
37
75
  ## Foundations
38
76
 
@@ -46,9 +84,9 @@ token so each theme can redefine it.
46
84
  paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
47
85
  the choice for this browser. Use the `light:` variant only for one-off values
48
86
  that no token covers, such as provider brand colors.
49
- - **Charts.** Use `var(--color-*)` for series colors in SVG attributes, not hex
50
- values, so they follow the theme.
51
-
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>)`.
52
90
  - **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
53
91
  `bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
54
92
  `surface-raised` (buttons, tooltips), `control`, `surface-hover`,
@@ -81,80 +119,53 @@ token so each theme can redefine it.
81
119
  use `rounded-lg`. The composer, menus, and transcript object cards use
82
120
  `rounded-xl`. Avatars and count badges use `rounded-full`.
83
121
 
84
- ## Shared class helpers
85
-
86
- `src/client/styles.ts` holds the few class strings that many components share.
87
-
88
- - `cn` joins class names.
89
- - `dashboardContainerClass` sets the page width.
90
- - `dashboardInlineLinkClass` styles a neutral underlined link inside prose.
91
- - `dashboardInteractiveTextClass` styles muted text that brightens on hover.
92
- - `dashboardComposerSurfaceClass` styles the composer surface.
122
+ Shared class strings live in `src/client/styles.ts`.
93
123
 
94
- ## Components
124
+ ## App components
95
125
 
96
- ### Primitives
97
-
98
- - **Actions.** `Button` (`default`, `primary`, `danger`, and `size="icon"`) and
99
- `ButtonLink`. Form pages use `Button tone="primary"` for their one main save
100
- action. Use `ToggleButton` variants (`pill`, `segment`, `text`) for pressed
101
- states, not one-off styles.
102
- - **Forms.** `Field` (`default`, `compact`), `TextInput`, `TextArea`, `Select`,
103
- and `SearchInput`. Use `TextInput size="comfortable"` for larger mobile
104
- controls. Use `TextArea prose` for instructions, not code. `Field error`
105
- renders an error at `<htmlFor>-error`. The owning control must set
106
- `aria-invalid` and `aria-describedby`. `Select` owns the native control and its
107
- inset arrow. Use its `filter` and `sort` variants for compact controls.
108
126
  - **Status.** Use `StatusDot` for dense table or list markers. Use `StatusChip`
109
- when the label must stay visible. `StatusChip` is a small dot and
110
- 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.
111
128
  - **Messages.** Use `Notice` for compact transient messages and `FormNotice`
112
129
  for wrapping form recovery text.
113
- - **Overlays.** `ui/sheet.tsx` is the shadcn/ui Base UI Sheet. It uses Base UI
114
- Dialog for focus and dismissal. `Drawer` adds a visible header and close
115
- control. `BottomSheet` puts details below a title and close control. Both use
116
- the app's shared scroll lock, skip motion when reduced motion is set, and
117
- close with the close button, backdrop, or Escape. Docked conversation details
118
- use a non-modal aside so the chat stays usable. `Tooltip` and
119
- `IconButtonTooltip` own hover text.
120
- - **Identity.** `ActorAvatar`, `ParticipantAvatarStack`, and `ObjectIcon`.
121
- `ObjectIcon` owns native Ticket, Code change, Automation, Deployment, and
122
- Item icons. It uses the shared type and status mapping and the original
123
- Octicons paths. Do not replace it with a provider logo or a status-only icon.
124
- - **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`
125
142
  variants for page bodies. Use `PageRouteLoading` when a page keeps its real
126
143
  header above a body skeleton. Use a feature-owned loading component for
127
144
  distinct geometry, such as conversations.
128
145
 
129
- ### Navigation
146
+ ## Navigation
130
147
 
131
148
  The dashboard has three navigation levels. Each has its own look, so the
132
149
  levels stay distinct.
133
150
 
134
151
  - **App navigation** in `layout/DashboardHeader` uses pills.
135
152
  - **Page links** use `SegmentedNav`, through `layout/SecondaryNavigation`. They
136
- are underline tabs that sit on the header border. With `fill`, links share
137
- the row width while their labels fit. Rows that do not fit scroll sideways.
138
- - **Panel switches** use `SegmentedTabs`, which draws a pill track. Pass the
139
- selected value, items, change handler, and panel content. It owns tab and
140
- panel labels and keyboard focus. Pass `scroll` to keep the tabs fixed while
141
- the panel content scrolls. That mode fills its frame and owns the content
142
- padding, so its scrollbar stays at the frame edge. `BottomSheet` content owns
143
- 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.
144
156
 
145
- Keep collection filters in `FilterTabList`.
157
+ Keep collection filters in `FilterTabList`. It uses `TabsList variant="line"`
158
+ and adds the count badges.
146
159
 
147
- ### Patterns
160
+ ## Patterns
148
161
 
149
162
  - **Layout.** `layout/` holds the shell chrome, page headers, cards, and
150
163
  section titles. `DashboardChrome` owns the sticky header block and its one
151
164
  bottom border.
152
- - **Directory tables and metrics.** `directory/` and `metrics/`.
153
- - **Charts.** Activity time-series charts own bucket labels in
154
- `charts/ActivityChart` (`formatActivityDate` for axes,
155
- `ActivityChartTooltip` and `formatActivityTooltipDate` for hover labels). New
156
- day or hour charts compose those helpers. They do not format UTC bucket keys
157
- 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.
158
169
  - **Conversation mobile geometry** is a hard ownership edge:
159
170
  - `layout/VisualViewportShell` owns the fixed shell, visualViewport CSS
160
171
  variables, and body lock.
@@ -166,16 +177,14 @@ Keep collection filters in `FilterTabList`.
166
177
  dock, and must not use `ComposerDock`. Do not add viewport height, offset, or
167
178
  bottom safe-area math in page modules.
168
179
 
169
- ### Features
170
-
171
180
  Feature surfaces stay under `conversations/` or `pages/` until two real callers
172
- share them. Transcript layout stays in `conversations/`. See
173
- [Transcript components](../conversations/README.md) for ownership.
181
+ share them. See [Transcript components](../conversations/README.md) for
182
+ transcript ownership.
174
183
 
175
184
  ## Rules
176
185
 
177
186
  - Prefer a named component on the second real use.
178
187
  - Keep Tailwind classes on the owning component.
179
- - Add gallery fixtures under `/dev/<section>` for new or changed shared
180
- 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.
181
190
  - One-off page glue may stay inline when extraction only adds indirection.
@@ -81,7 +81,7 @@ function edgeFade(edges: HiddenEdges): CSSProperties | undefined {
81
81
  /**
82
82
  * Show links between sibling pages as underline tabs. The current page has
83
83
  * a solid underline that sits on the border below the row. App navigation
84
- * uses pills and in-place panel switches use `SegmentedTabs`, so the three
84
+ * uses pills and in-place panel switches use `ui/tabs`, so the three
85
85
  * levels stay distinct.
86
86
  *
87
87
  * With `fill`, links share the row width equally while their labels fit.
@@ -2,11 +2,7 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import { cn } from "../styles";
4
4
 
5
- /** Shared running-text shimmer used by live metrics and in-progress tool names. */
6
- export const shimmerTextClassName =
7
- "animate-[junior-text-shimmer_1.6s_linear_infinite] bg-[linear-gradient(90deg,#777_0%,#d6d6d6_40%,#fff_50%,#d6d6d6_60%,#777_100%)] bg-[length:200%_100%] bg-clip-text text-transparent motion-reduce:animate-none";
8
-
9
- /** Render text with the dashboard running shimmer when `active` is true. */
5
+ /** Render text with the shadcn `shimmer` utility when `active` is true. */
10
6
  export function ShimmerText(props: {
11
7
  active?: boolean;
12
8
  as?: "span" | "strong";
@@ -19,7 +15,7 @@ export function ShimmerText(props: {
19
15
  if (!props.active && !props.className && !props["aria-label"]) {
20
16
  return props.children;
21
17
  }
22
- const className = cn(props.active && shimmerTextClassName, props.className);
18
+ const className = cn(props.active && "shimmer", props.className);
23
19
  if (props.as === "strong") {
24
20
  return (
25
21
  <strong aria-label={props["aria-label"]} className={className}>