@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
@@ -3,7 +3,8 @@ import { Monitor, Moon, Sun, type LucideIcon } from "lucide-react";
3
3
  import type { DashboardThemeChoice } from "../../theme";
4
4
  import { cn } from "../styles";
5
5
  import { useDashboardThemeChoice } from "../theme";
6
- import { ToggleButton } from "./Button";
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
7
+ import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
7
8
 
8
9
  const themeChoices = [
9
10
  { Icon: Monitor, label: "System", value: "system" },
@@ -25,24 +26,27 @@ export function ThemeSwitcher(props: { className?: string }) {
25
26
  <span className="font-sans text-sm font-semibold text-dashboard-text">
26
27
  Theme
27
28
  </span>
28
- <div
29
+ <ToggleGroup
29
30
  aria-label="Theme"
30
- className="flex gap-0.5 rounded-lg border border-dashboard-border p-0.5"
31
- role="group"
31
+ className="gap-0.5 rounded-lg border border-dashboard-border p-0.5"
32
+ onValueChange={([next]) => {
33
+ if (next) setChoice(next);
34
+ }}
35
+ value={[choice]}
32
36
  >
33
37
  {themeChoices.map(({ Icon, label, value }) => (
34
- <ToggleButton
35
- aria-label={`${label} theme`}
36
- key={value}
37
- onClick={() => setChoice(value)}
38
- pressed={choice === value}
39
- title={label}
40
- variant="segment"
41
- >
42
- <Icon aria-hidden="true" size={14} strokeWidth={2} />
43
- </ToggleButton>
38
+ <Tooltip key={value}>
39
+ <TooltipTrigger
40
+ render={
41
+ <ToggleGroupItem aria-label={`${label} theme`} value={value}>
42
+ <Icon aria-hidden="true" size={14} strokeWidth={2} />
43
+ </ToggleGroupItem>
44
+ }
45
+ />
46
+ <TooltipContent size="compact">{label}</TooltipContent>
47
+ </Tooltip>
44
48
  ))}
45
- </div>
49
+ </ToggleGroup>
46
50
  </div>
47
51
  );
48
52
  }
@@ -1,62 +1,21 @@
1
1
  import {
2
2
  cloneElement,
3
- createContext,
4
3
  type CSSProperties,
5
4
  type ReactElement,
6
5
  type ReactNode,
7
- type SVGProps,
8
- useContext,
9
- useLayoutEffect,
10
- useRef,
11
- useState,
12
6
  } from "react";
7
+ import { ReferenceLine, usePlotArea, XAxis, YAxis } from "recharts";
13
8
 
14
- import { Tooltip } from "../Tooltip";
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipTitle,
13
+ TooltipTrigger,
14
+ tooltipContentVariants,
15
+ } from "../ui/tooltip";
15
16
  import { getDashboardTimeZone } from "../../format";
16
17
  import { cn } from "../../styles";
17
18
 
18
- /** Shared SVG dimensions and derived plot bounds for activity charts. */
19
- export type ActivityChartLayout = {
20
- bottom: number;
21
- height: number;
22
- left: number;
23
- plotHeight: number;
24
- plotWidth: number;
25
- right: number;
26
- top: number;
27
- width: number;
28
- };
29
-
30
- type LayoutOptions = {
31
- bottom?: number;
32
- left?: number;
33
- right?: number;
34
- top?: number;
35
- width?: number;
36
- };
37
-
38
- /** Create the shared plot dimensions used by dashboard activity charts. */
39
- export function createActivityChartLayout(
40
- height: number,
41
- options?: LayoutOptions,
42
- ): ActivityChartLayout {
43
- const width = options?.width ?? 960;
44
- const left = options?.left ?? 56;
45
- const right = options?.right ?? 18;
46
- const top = options?.top ?? 24;
47
- const bottom = options?.bottom ?? 36;
48
- return {
49
- bottom,
50
- height,
51
- left,
52
- plotHeight: height - top - bottom,
53
- plotWidth: width - left - right,
54
- right,
55
- top,
56
- width,
57
- };
58
- }
59
-
60
19
  /** True when the bucket key is a UTC hour (`YYYY-MM-DDTHH`). */
61
20
  export function isActivityHourBucket(date: string): boolean {
62
21
  return /^\d{4}-\d{2}-\d{2}T\d{2}$/.test(date);
@@ -121,39 +80,11 @@ export function formatActivityCategoryTooltipLabel(label: string): string {
121
80
  }
122
81
 
123
82
  /** Choose the first, middle, and last available chart labels. */
124
- export function activityLabelIndexes(count: number): number[] {
125
- return [...new Set([0, Math.floor((count - 1) / 2), count - 1])].filter(
126
- (index) => index >= 0,
127
- );
128
- }
129
-
130
- const chartAxisFontSizePx = 11;
131
- const ChartSvgScaleContext = createContext(1);
132
-
133
- /** Shared visual style for SVG chart axis labels. */
134
- export const chartAxisLabelClassName =
135
- "fill-dashboard-text-muted font-sans tabular-nums leading-none text-dashboard-text-muted";
136
-
137
- type ChartAxisLabelProps = Omit<
138
- SVGProps<SVGTextElement>,
139
- "children" | "className" | "fill" | "fontFamily" | "fontSize"
140
- > & {
141
- children: ReactNode;
142
- };
143
-
144
- /** Render an SVG axis label at the shared on-screen size. */
145
- export function ChartAxisLabel(props: ChartAxisLabelProps) {
146
- const { children, ...textProps } = props;
147
- const scale = useContext(ChartSvgScaleContext);
148
- return (
149
- <text
150
- className={chartAxisLabelClassName}
151
- fontSize={chartAxisFontSizePx / scale}
152
- {...textProps}
153
- >
154
- {children}
155
- </text>
156
- );
83
+ function activityLabelIndexes(count: number): number[] {
84
+ if (count === 0) {
85
+ return [];
86
+ }
87
+ return [...new Set([0, Math.floor((count - 1) / 2), count - 1])];
157
88
  }
158
89
 
159
90
  /** Shared HTML axis label for non-SVG chart axes. */
@@ -175,151 +106,76 @@ export function ChartAxisHtmlLabel(props: {
175
106
  );
176
107
  }
177
108
 
178
- type ChartBox = { height: number; width: number };
179
-
180
- /**
181
- * Screen pixels per SVG user unit. The default `preserveAspectRatio` fits the
182
- * whole viewBox inside the element box, so the smaller ratio wins.
183
- */
184
- function chartSvgScale(box: ChartBox | undefined, layout: ActivityChartLayout) {
185
- if (!box || box.width <= 0 || box.height <= 0) return 1;
186
- return Math.min(box.width / layout.width, box.height / layout.height);
187
- }
109
+ /** Plot margins shared by dashboard Recharts charts. */
110
+ export const activityChartMargin = { bottom: 0, left: 4, right: 12, top: 12 };
188
111
 
189
- /**
190
- * Render the shared SVG chart shell and provide its live screen scale to labels.
191
- * SVG text uses user units, so labels invert this scale to remain 12px on screen.
192
- * The scale comes from the current element size and viewBox, so a measurement
193
- * from an earlier layout is never reused.
194
- */
195
- export function ChartSvg(props: {
196
- "aria-label": string;
197
- children: ReactNode;
198
- className?: string;
199
- layout: ActivityChartLayout;
112
+ /** Render the first, middle, and last bucket labels under a Recharts chart. */
113
+ export function ActivityChartXAxis(props: {
114
+ /** Center every label, for categories that are not a time series. */
115
+ centered?: boolean;
116
+ dataKey?: string;
117
+ formatLabel?(label: string): string;
118
+ keys: readonly string[];
200
119
  }) {
201
- const svgRef = useRef<SVGSVGElement>(null);
202
- const [box, setBox] = useState<ChartBox>();
203
-
204
- useLayoutEffect(() => {
205
- const svg = svgRef.current;
206
- if (!svg) return;
207
- const updateBox = (next: ChartBox) =>
208
- setBox((previous) =>
209
- previous?.width === next.width && previous.height === next.height
210
- ? previous
211
- : next,
212
- );
213
- // Measure before the first paint; the observer reports later changes.
214
- updateBox({ height: svg.clientHeight, width: svg.clientWidth });
215
- const observer = new ResizeObserver(([entry]) => {
216
- if (entry) updateBox(entry.contentRect);
217
- });
218
- observer.observe(svg);
219
- return () => observer.disconnect();
220
- }, []);
221
-
222
- const scale = chartSvgScale(box, props.layout);
223
-
120
+ const formatLabel = props.formatLabel ?? formatActivityCategoryLabel;
121
+ const ticks = activityLabelIndexes(props.keys.length).map(
122
+ (index) => props.keys[index]!,
123
+ );
124
+ const first = props.keys[0];
125
+ const last = props.keys.at(-1);
224
126
  return (
225
- <ChartSvgScaleContext value={scale}>
226
- <svg
227
- aria-label={props["aria-label"]}
228
- className={cn("block h-auto w-full", props.className)}
229
- ref={svgRef}
230
- role="img"
231
- viewBox={`0 0 ${props.layout.width} ${props.layout.height}`}
232
- >
233
- {props.children}
234
- </svg>
235
- </ChartSvgScaleContext>
127
+ <XAxis
128
+ axisLine={false}
129
+ dataKey={props.dataKey ?? "date"}
130
+ interval={0}
131
+ tick={({ payload, x, y }) => {
132
+ const value = String(payload.value);
133
+ const anchor = props.centered
134
+ ? "middle"
135
+ : value === first
136
+ ? "start"
137
+ : value === last
138
+ ? "end"
139
+ : "middle";
140
+ return (
141
+ <text
142
+ className="fill-dashboard-text-muted"
143
+ dy="0.71em"
144
+ textAnchor={anchor}
145
+ x={x}
146
+ y={y}
147
+ >
148
+ {formatLabel(value)}
149
+ </text>
150
+ );
151
+ }}
152
+ tickLine={false}
153
+ tickMargin={8}
154
+ ticks={ticks}
155
+ />
236
156
  );
237
157
  }
238
158
 
239
- /** Render the shared horizontal guide lines and values for activity charts. */
240
- export function ActivityChartGrid(props: {
159
+ /** Render minimum, middle, and maximum guide values on a Recharts chart. */
160
+ export function ActivityChartYAxis(props: {
241
161
  format?(value: number): string;
242
- layout: ActivityChartLayout;
243
162
  maximum: number;
163
+ minimum?: number;
244
164
  }) {
165
+ const minimum = props.minimum ?? 0;
245
166
  const format = props.format ?? ((value: number) => String(Math.round(value)));
246
- return [0, 0.5, 1].map((ratio) => {
247
- const yPosition = props.layout.top + ratio * props.layout.plotHeight;
248
- return (
249
- <g key={ratio}>
250
- <line
251
- stroke="var(--color-dashboard-border-subtle)"
252
- strokeDasharray="3 5"
253
- x1={props.layout.left}
254
- x2={props.layout.width - props.layout.right}
255
- y1={yPosition}
256
- y2={yPosition}
257
- />
258
- <ChartAxisLabel
259
- textAnchor="end"
260
- x={props.layout.left - 8}
261
- y={yPosition + 3}
262
- >
263
- {format(props.maximum * (1 - ratio))}
264
- </ChartAxisLabel>
265
- </g>
266
- );
267
- });
268
- }
269
-
270
- /** Render the shared first, middle, and last date labels for activity charts. */
271
- export function ActivityChartDateLabels(props: {
272
- dates: readonly string[];
273
- formatDate?(date: string): string;
274
- layout: ActivityChartLayout;
275
- xPosition(index: number): number;
276
- }) {
277
- const formatDate = props.formatDate ?? formatActivityDate;
278
- return activityLabelIndexes(props.dates.length).map((index) => {
279
- const date = props.dates[index];
280
- if (!date) return null;
281
- return (
282
- <ChartAxisLabel
283
- key={date}
284
- textAnchor={
285
- index === 0
286
- ? "start"
287
- : index === props.dates.length - 1
288
- ? "end"
289
- : "middle"
290
- }
291
- x={props.xPosition(index)}
292
- y={props.layout.height - 8}
293
- >
294
- {formatDate(date)}
295
- </ChartAxisLabel>
296
- );
297
- });
298
- }
299
-
300
- /** Render centered categorical labels using stable category keys. */
301
- export function ChartCategoryLabels(props: {
302
- categories: ReadonlyArray<{ id: string; label: string }>;
303
- formatLabel?(label: string): string;
304
- layout: ActivityChartLayout;
305
- xPosition(index: number): number;
306
- }) {
307
- // Default formats day/hour bucket keys; leave other category labels alone.
308
- const formatLabel = props.formatLabel ?? formatActivityCategoryLabel;
309
- return activityLabelIndexes(props.categories.length).map((index) => {
310
- const category = props.categories[index];
311
- if (!category) return null;
312
- return (
313
- <ChartAxisLabel
314
- key={category.id}
315
- textAnchor="middle"
316
- x={props.xPosition(index)}
317
- y={props.layout.height - 8}
318
- >
319
- {formatLabel(category.label)}
320
- </ChartAxisLabel>
321
- );
322
- });
167
+ return (
168
+ <YAxis
169
+ axisLine={false}
170
+ domain={[minimum, props.maximum]}
171
+ interval={0}
172
+ tickFormatter={format}
173
+ tickLine={false}
174
+ tickMargin={8}
175
+ ticks={[minimum, (minimum + props.maximum) / 2, props.maximum]}
176
+ width="auto"
177
+ />
178
+ );
323
179
  }
324
180
 
325
181
  /** Mean of chart bucket values; empty input is 0. */
@@ -328,92 +184,122 @@ export function activityChartAverage(values: readonly number[]): number {
328
184
  return values.reduce((sum, value) => sum + value, 0) / values.length;
329
185
  }
330
186
 
331
- /** Shared visual style for average-line labels. */
332
- export const chartAverageLabelClassName =
333
- "fill-dashboard-text-solid font-sans tabular-nums leading-none";
334
-
335
187
  /**
336
- * Opt-in horizontal average across the plotted buckets.
337
- * Renders a dashed guide plus a compact "value / day" label on the right.
338
- * Label sits on a dark chip so series-colored bars and guides stay off the text.
188
+ * Label for an average line, such as `3.2 / day`.
189
+ * Returns nothing for a zero mean or when the formatter floors it to zero.
339
190
  */
340
- export function ActivityChartAverageLine(props: {
341
- average: number;
342
- format(value: number): string;
343
- layout: ActivityChartLayout;
344
- maximum: number;
345
- stroke?: string;
346
- unit?: string;
347
- }) {
348
- const scale = useContext(ChartSvgScaleContext);
349
- if (!(props.average > 0) || !(props.maximum > 0)) return null;
350
-
351
- const unit = props.unit ?? "day";
352
- const formatted = props.format(props.average).trim();
353
- // Flooring formatters can turn a real sub-1 mean into "0"; hide that case.
354
- if (!formatted || /^\$?0(?:\.0+)?(?:ms)?$/i.test(formatted)) return null;
191
+ export function activityChartAverageLabel(
192
+ average: number,
193
+ format: (value: number) => string,
194
+ unit = "day",
195
+ ): string | undefined {
196
+ if (!(average > 0)) return undefined;
197
+ const formatted = format(average).trim();
198
+ if (!formatted || /^\$?0(?:\.0+)?(?:ms)?$/i.test(formatted)) return undefined;
199
+ return `${formatted} / ${unit}`;
200
+ }
355
201
 
356
- const y =
357
- props.layout.top +
358
- props.layout.plotHeight -
359
- (props.average / props.maximum) * props.layout.plotHeight;
360
- const x1 = props.layout.left;
361
- const x2 = props.layout.width - props.layout.right;
362
- const label = `${formatted} / ${unit}`;
363
- const fontSize = chartAxisFontSizePx / scale;
364
- // Mono width estimate for the chip and dashed-guide stop.
365
- const labelWidth = Math.max(52, label.length * fontSize * 0.68);
366
- const chipPadX = 5;
367
- const chipPadY = 3;
368
- const chipHeight = fontSize + chipPadY * 2;
369
- const chipWidth = labelWidth + chipPadX * 2;
370
- const chipX = x2 - chipWidth;
371
- // Keep the chip inside the plot; flip below the line near the top edge.
372
- const labelAbove = y - props.layout.top > chipHeight + 4;
373
- const chipY = labelAbove ? y - chipHeight - 3 : y + 4;
374
- const textY = chipY + chipHeight / 2 + fontSize * 0.35;
375
- const lineEnd = Math.max(x1, chipX - 6);
202
+ const AVERAGE_LABEL_FONT_PX = 12;
376
203
 
204
+ /**
205
+ * Draw the average label on a solid chip at the right end of the line, so
206
+ * series colors stay off the text. The chip flips below the line near the top.
207
+ */
208
+ function ActivityChartAverageChip(props: {
209
+ text: string;
210
+ viewBox?: { width?: number; x?: number; y?: number };
211
+ }) {
212
+ const plotArea = usePlotArea();
213
+ const lineY = props.viewBox?.y ?? 0;
214
+ const right = (props.viewBox?.x ?? 0) + (props.viewBox?.width ?? 0);
215
+ const chipHeight = AVERAGE_LABEL_FONT_PX + 6;
216
+ // Width estimate for tabular digits at the label size.
217
+ const chipWidth =
218
+ Math.max(52, props.text.length * AVERAGE_LABEL_FONT_PX * 0.62) + 10;
219
+ const above = lineY - (plotArea?.y ?? 0) > chipHeight + 4;
220
+ const chipY = above ? lineY - chipHeight - 3 : lineY + 4;
377
221
  return (
378
- <g aria-label={`average ${label}`} pointerEvents="none">
379
- <line
380
- stroke={props.stroke ?? "var(--color-dashboard-text-faint)"}
381
- strokeDasharray="4 4"
382
- strokeOpacity={0.85}
383
- strokeWidth={1.25}
384
- x1={x1}
385
- x2={lineEnd}
386
- y1={y}
387
- y2={y}
388
- />
389
- {/* Solid chip so series colors never wash out the label text. */}
222
+ <g pointerEvents="none">
390
223
  <rect
391
224
  fill="var(--color-dashboard-bg-elevated)"
392
225
  fillOpacity={0.92}
393
226
  height={chipHeight}
394
227
  rx={3}
395
228
  width={chipWidth}
396
- x={chipX}
229
+ x={right - chipWidth}
397
230
  y={chipY}
398
231
  />
399
232
  <text
400
- className={chartAverageLabelClassName}
401
- fill="var(--color-dashboard-text-solid)"
402
- fontSize={fontSize}
233
+ className="fill-dashboard-text-solid font-sans tabular-nums"
234
+ dominantBaseline="central"
403
235
  textAnchor="middle"
404
- x={chipX + chipWidth / 2}
405
- y={textY}
236
+ x={right - chipWidth / 2}
237
+ y={chipY + chipHeight / 2}
406
238
  >
407
- {label}
239
+ {props.text}
408
240
  </text>
409
241
  </g>
410
242
  );
411
243
  }
412
244
 
413
- /** Render label and value rows inside an activity-chart tooltip. */
414
- export function ActivityTooltipRows(props: {
415
- rows: ReadonlyArray<readonly [label: ReactNode, value: ReactNode]>;
245
+ /**
246
+ * Opt-in horizontal average across the plotted buckets.
247
+ * Renders a dashed guide plus a compact "value / day" label on the right.
248
+ */
249
+ export function ActivityChartAverageLine(props: {
250
+ average: number;
251
+ format(value: number): string;
252
+ stroke?: string;
253
+ unit?: string;
416
254
  }) {
255
+ const label = activityChartAverageLabel(
256
+ props.average,
257
+ props.format,
258
+ props.unit,
259
+ );
260
+ if (!label) return null;
261
+ return (
262
+ <ReferenceLine
263
+ aria-label={`average ${label}`}
264
+ ifOverflow="extendDomain"
265
+ label={<ActivityChartAverageChip text={label} />}
266
+ stroke={props.stroke ?? "var(--color-dashboard-text-faint)"}
267
+ strokeDasharray="4 4"
268
+ strokeOpacity={0.85}
269
+ strokeWidth={1.25}
270
+ y={props.average}
271
+ />
272
+ );
273
+ }
274
+
275
+ type TooltipRows = ReadonlyArray<readonly [label: ReactNode, value: ReactNode]>;
276
+
277
+ /**
278
+ * Recharts tooltip body for one activity bucket. It titles the bucket with
279
+ * `formatLabel`, or by default `formatActivityCategoryTooltipLabel`, and lists
280
+ * `rows` for the hovered datum.
281
+ * Pass it as `content` of `ChartTooltip`; Recharts injects the other props.
282
+ */
283
+ export function ActivityChartTooltipContent<Datum>(props: {
284
+ active?: boolean;
285
+ formatLabel?(label: string): string;
286
+ label?: unknown;
287
+ payload?: ReadonlyArray<{ payload?: unknown }>;
288
+ rows(datum: Datum): TooltipRows;
289
+ }) {
290
+ const datum = props.payload?.[0]?.payload as Datum | undefined;
291
+ if (!props.active || datum === undefined) return null;
292
+ const formatLabel = props.formatLabel ?? formatActivityCategoryTooltipLabel;
293
+ return (
294
+ <div className={tooltipContentVariants()} role="status">
295
+ <TooltipTitle>{formatLabel(String(props.label))}</TooltipTitle>
296
+ <ActivityTooltipRows rows={props.rows(datum)} />
297
+ </div>
298
+ );
299
+ }
300
+
301
+ /** Render label and value rows inside an activity-chart tooltip. */
302
+ export function ActivityTooltipRows(props: { rows: TooltipRows }) {
417
303
  return (
418
304
  <div className="grid grid-cols-[auto_auto] gap-x-4 gap-y-0.5">
419
305
  {props.rows.flatMap(([label, value], index) => [
@@ -439,7 +325,6 @@ export function ActivityChartTooltip(props: {
439
325
  content: ReactNode;
440
326
  date: string;
441
327
  summary?: string;
442
- triggerClassName?: string;
443
328
  }) {
444
329
  const dateLabel = formatActivityTooltipDate(props.date);
445
330
  const child = props.summary
@@ -449,12 +334,12 @@ export function ActivityChartTooltip(props: {
449
334
  : props.children;
450
335
 
451
336
  return (
452
- <Tooltip
453
- content={props.content}
454
- label={dateLabel}
455
- triggerClassName={props.triggerClassName}
456
- >
457
- {child}
337
+ <Tooltip>
338
+ <TooltipTrigger render={child} />
339
+ <TooltipContent>
340
+ <TooltipTitle>{dateLabel}</TooltipTitle>
341
+ {props.content}
342
+ </TooltipContent>
458
343
  </Tooltip>
459
344
  );
460
345
  }