@sentry/junior-dashboard 0.264.0 → 0.266.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 (213) 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/ActorAvatar.tsx +10 -22
  14. package/src/client/components/BottomSheet.tsx +12 -2
  15. package/src/client/components/ConnectionBanner.tsx +4 -3
  16. package/src/client/components/CopyButton.tsx +82 -0
  17. package/src/client/components/DetailsPanel.tsx +133 -0
  18. package/src/client/components/Drawer.tsx +15 -24
  19. package/src/client/components/EmptyTelemetry.tsx +5 -3
  20. package/src/client/components/FilterBar.tsx +16 -31
  21. package/src/client/components/ImageAttachment.tsx +49 -63
  22. package/src/client/components/InlineError.tsx +8 -4
  23. package/src/client/components/Metric.tsx +33 -23
  24. package/src/client/components/Notice.tsx +19 -26
  25. package/src/client/components/PageContentSkeleton.tsx +1 -1
  26. package/src/client/components/Pagination.tsx +27 -27
  27. package/src/client/components/ParticipantAvatarStack.tsx +43 -39
  28. package/src/client/components/ProfileMenu.tsx +27 -93
  29. package/src/client/components/README.md +103 -78
  30. package/src/client/components/SearchInput.tsx +51 -41
  31. package/src/client/components/SegmentedNav.tsx +1 -1
  32. package/src/client/components/ShimmerText.tsx +2 -6
  33. package/src/client/components/ThemeSwitcher.tsx +19 -15
  34. package/src/client/components/VersionDriftBanner.tsx +9 -7
  35. package/src/client/components/charts/ActivityChart.tsx +173 -288
  36. package/src/client/components/charts/CacheWrites.tsx +59 -37
  37. package/src/client/components/charts/InputCacheChart.tsx +137 -131
  38. package/src/client/components/charts/README.md +4 -4
  39. package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
  40. package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
  41. package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
  42. package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
  43. package/src/client/components/layout/DashboardHeader.tsx +44 -90
  44. package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
  45. package/src/client/components/layout/VisualViewportShell.tsx +2 -2
  46. package/src/client/components/ui/accordion.tsx +77 -0
  47. package/src/client/components/ui/alert-dialog.tsx +89 -0
  48. package/src/client/components/ui/alert.tsx +64 -0
  49. package/src/client/components/ui/avatar.tsx +53 -0
  50. package/src/client/components/ui/button.tsx +86 -0
  51. package/src/client/components/ui/chart.tsx +74 -0
  52. package/src/client/components/ui/checkbox.tsx +26 -0
  53. package/src/client/components/ui/collapsible.tsx +22 -0
  54. package/src/client/components/ui/dialog.tsx +66 -0
  55. package/src/client/components/ui/empty.tsx +32 -0
  56. package/src/client/components/ui/field.tsx +49 -0
  57. package/src/client/components/ui/input-group.tsx +96 -0
  58. package/src/client/components/ui/input.tsx +35 -0
  59. package/src/client/components/ui/native-select.tsx +57 -0
  60. package/src/client/components/ui/pagination.tsx +60 -0
  61. package/src/client/components/ui/popover.tsx +67 -0
  62. package/src/client/components/ui/radio-group.tsx +32 -0
  63. package/src/client/components/ui/sheet.tsx +13 -3
  64. package/src/client/components/ui/skeleton.tsx +19 -0
  65. package/src/client/components/ui/spinner.tsx +21 -0
  66. package/src/client/components/ui/table.tsx +101 -0
  67. package/src/client/components/ui/tabs.tsx +75 -0
  68. package/src/client/components/ui/textarea.tsx +17 -0
  69. package/src/client/components/ui/toast.tsx +215 -0
  70. package/src/client/components/ui/toggle-group.tsx +36 -0
  71. package/src/client/components/ui/toggle.tsx +37 -0
  72. package/src/client/components/ui/tooltip.tsx +168 -0
  73. package/src/client/components/ui/trace.tsx +96 -0
  74. package/src/client/conversations/ChatLayout.tsx +7 -3
  75. package/src/client/conversations/ConversationBrief.tsx +17 -6
  76. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  77. package/src/client/conversations/ConversationComposer.tsx +26 -12
  78. package/src/client/conversations/ConversationDetails.tsx +147 -62
  79. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  80. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  81. package/src/client/conversations/ConversationHeader.tsx +13 -7
  82. package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
  83. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  84. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  85. package/src/client/conversations/ConversationMeta.tsx +55 -88
  86. package/src/client/conversations/ConversationPage.tsx +220 -101
  87. package/src/client/conversations/ConversationSidebar.tsx +49 -29
  88. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  89. package/src/client/conversations/ConversationWatches.tsx +173 -0
  90. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  91. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  92. package/src/client/conversations/DetailsList.tsx +139 -0
  93. package/src/client/conversations/EventDetails.tsx +42 -8
  94. package/src/client/conversations/EventUsage.tsx +99 -74
  95. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  96. package/src/client/conversations/ObjectCard.tsx +20 -8
  97. package/src/client/conversations/PendingMailboxStack.tsx +52 -38
  98. package/src/client/conversations/README.md +89 -13
  99. package/src/client/conversations/ShareConversationButton.tsx +28 -17
  100. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  101. package/src/client/conversations/ToolFrame.tsx +36 -52
  102. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  103. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  104. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  105. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  106. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  107. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  108. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  109. package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
  110. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  111. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  112. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  113. package/src/client/conversations/TranscriptSummary.tsx +34 -2
  114. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  115. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  116. package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
  117. package/src/client/conversations/TranscriptView.tsx +63 -27
  118. package/src/client/conversations/eventLog.ts +58 -0
  119. package/src/client/conversations/eventTime.ts +60 -0
  120. package/src/client/conversations/queries.ts +29 -0
  121. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  122. package/src/client/conversations/transcriptMentions.tsx +18 -5
  123. package/src/client/format.ts +5 -0
  124. package/src/client/pages/ConnectionsPage.tsx +2 -2
  125. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  126. package/src/client/pages/SettingsPage.tsx +9 -10
  127. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  128. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  129. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  130. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  131. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  132. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  133. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  134. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  135. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  136. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  137. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  138. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  139. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  140. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  141. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  142. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  143. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  144. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  145. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  146. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  147. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  148. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  149. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  150. package/src/client/pages/automations/README.md +57 -0
  151. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  152. package/src/client/pages/automations/automationFormat.ts +37 -23
  153. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  154. package/src/client/pages/code/CodePage.tsx +52 -55
  155. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  156. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  157. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  158. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  159. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  160. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  161. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  162. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  163. package/src/client/pages/memory/memoryRecord.ts +1 -1
  164. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  165. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  166. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  167. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  168. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  169. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  170. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  171. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  172. package/src/client/pages/system/README.md +23 -0
  173. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  174. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  175. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  176. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  177. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  178. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  179. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  180. package/src/client/pages/system/sandboxSize.ts +8 -0
  181. package/src/client/pages/system/workspaceQueries.ts +53 -0
  182. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  183. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  184. package/src/client/styles.ts +4 -0
  185. package/src/client.tsx +7 -1
  186. package/src/mock-reporting/automation-collection.ts +4 -2
  187. package/src/mock-reporting/fixtures.ts +23 -7
  188. package/src/mock-reporting/routes.ts +12 -0
  189. package/src/mock-reporting/watches.ts +94 -0
  190. package/src/shell.ts +4 -9
  191. package/src/tailwind.css +0 -10
  192. package/src/client/components/Button.tsx +0 -118
  193. package/src/client/components/Field.tsx +0 -53
  194. package/src/client/components/SegmentedTabs.tsx +0 -119
  195. package/src/client/components/Select.tsx +0 -52
  196. package/src/client/components/Skeleton.tsx +0 -17
  197. package/src/client/components/TextInput.tsx +0 -58
  198. package/src/client/components/Tooltip.tsx +0 -216
  199. package/src/client/components/charts/useChartLayout.ts +0 -27
  200. package/src/client/components/ui/sheet.LICENSE +0 -21
  201. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
  202. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  203. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  204. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  205. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  206. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  207. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  208. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  209. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  210. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  211. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  212. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  213. package/src/client/pages/system/SnapshotSummary.tsx +0 -78
@@ -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,76 @@ 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
+ | `ActorAvatar` | `ui/avatar` |
39
+ | `ParticipantAvatarStack` | `ui/avatar` |
40
+ | `SearchInput` | `ui/input-group` |
41
+ | `PagePagination` | `ui/pagination` |
42
+ | `EmptyTelemetry` | `ui/empty` |
43
+ | `TimeRangeSelector` | `ui/toggle-group` |
44
+ | `ThemeSwitcher` | `ui/toggle-group` |
45
+ | `TranscriptViewToggle` | `ui/toggle-group` |
46
+ | `FilterTabList` | `ui/tabs` |
47
+ | `charts/ActivityChartTooltip` | `ui/tooltip` |
48
+ | `conversations/ToolFrame` | `ui/trace` |
49
+ | `TranscriptReasoningView` | `ui/trace` |
50
+ | `ImageAttachment` (preview) | `ui/dialog` |
51
+ | `Notice`, `InlineError` | `ui/alert` |
52
+ | App banners | `ui/alert` |
53
+ | Recharts activity charts | `ui/chart` |
54
+
55
+ Rules for adopted components:
56
+
57
+ - Use `ui/trace` for transcript activity rows. It composes `ui/collapsible`.
58
+ A closed tool row unmounts its panel, so it does not run Shiki. Reasoning
59
+ sets `hiddenUntilFound`, so browser find reaches closed text. Transcript
60
+ search holds rows open and turns off the row button. Put only inline
61
+ elements, such as `span`, inside the row button.
62
+ - Use `ui/alert-dialog` for every confirmation. Do not use `window.confirm`.
63
+ Keep the confirmed item in state after close, so the closing dialog keeps its
64
+ text.
65
+ - Use `useSheet` for each app `ui/dialog`, as for sheets. It shares the
66
+ document scroll lock. Only the front modal closes on Escape or a backdrop
67
+ tap. Mount the dialog only while it is open.
68
+ - `ui/alert` sets `role="alert"`. Pass `role="status"` for a message that is
69
+ not an error.
70
+ - `ui/toast` has no global notification manager. Features call
71
+ `useToastManager` inside the mounted `Toaster` provider.
72
+ - Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
73
+ `aria-label`.
74
+ - Give an icon-only control its label with `TooltipContent size="compact"`.
75
+ Do not use the native `title` attribute. Tooltips open on mouse hover and
76
+ keyboard focus only, so a tap still runs the control.
77
+ - When a `ui/button` tooltip trigger can be disabled, set
78
+ `focusableWhenDisabled`. A native disabled button can drop hover and focus,
79
+ so Firefox closes the tooltip and hides pending or result labels.
80
+ `ui/button` styles `data-disabled`, so both disabled modes look the same.
81
+ - Build a form field from `ui/field`. Give `FieldError` the id
82
+ `<control id>-error` and `FieldDescription` the id `<control id>-help`. The
83
+ control sets `aria-invalid` and `aria-describedby`.
84
+ - Use `Button size="menu"` for a full-width icon and label row in a mobile
85
+ overflow menu. A pressed row adds `bg-dashboard-fill-strong`.
86
+ - For a link that looks like a button, put `buttonVariants()` on the router
87
+ `Link`. A form page has one `Button variant="primary"` for its main save
88
+ action.
36
89
 
37
90
  ## Foundations
38
91
 
@@ -46,9 +99,9 @@ token so each theme can redefine it.
46
99
  paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
47
100
  the choice for this browser. Use the `light:` variant only for one-off values
48
101
  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
-
102
+ - **Charts.** Use `var(--color-*)` for series colors, not hex values, so they
103
+ follow the theme. Name series colors in the `ChartContainer` config and fill
104
+ each series with `var(--color-<key>)`.
52
105
  - **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
53
106
  `bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
54
107
  `surface-raised` (buttons, tooltips), `control`, `surface-hover`,
@@ -81,80 +134,54 @@ token so each theme can redefine it.
81
134
  use `rounded-lg`. The composer, menus, and transcript object cards use
82
135
  `rounded-xl`. Avatars and count badges use `rounded-full`.
83
136
 
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.
137
+ Shared class strings live in `src/client/styles.ts`.
93
138
 
94
- ## Components
139
+ ## App components
95
140
 
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
141
  - **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.
142
+ when the label must stay visible. `StatusChip` is not a control.
111
143
  - **Messages.** Use `Notice` for compact transient messages and `FormNotice`
112
144
  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`
145
+ - **Disclosure.** Use `ui/collapsible` for one show-or-hide section. Put a
146
+ `ChevronRight` with `dashboardDisclosureChevronClass` in a `group` trigger.
147
+ Set `hiddenUntilFound` on the content when browser find must reach closed
148
+ text. Closed content then keeps its box, so put margins and padding on a
149
+ child. Use `ui/accordion` for a set of sections with headings.
150
+ - **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
151
+ `BottomSheet` below that width. It owns Escape and the scroll frame for
152
+ docked panels.
153
+ - **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
154
+ Deployment, and Item icons. Do not replace it with a provider logo or a
155
+ status-only icon.
156
+ - **Loading.** Use `ui/skeleton` for placeholder leaves and `PageContentSkeleton`
125
157
  variants for page bodies. Use `PageRouteLoading` when a page keeps its real
126
158
  header above a body skeleton. Use a feature-owned loading component for
127
- distinct geometry, such as conversations.
159
+ distinct geometry, such as conversations. Use `ui/spinner` for an inline
160
+ pending icon.
128
161
 
129
- ### Navigation
162
+ ## Navigation
130
163
 
131
164
  The dashboard has three navigation levels. Each has its own look, so the
132
165
  levels stay distinct.
133
166
 
134
167
  - **App navigation** in `layout/DashboardHeader` uses pills.
135
168
  - **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.
169
+ are underline tabs that sit on the header border.
170
+ - **Panel switches** use `ui/tabs`. Give `TabsList` an `aria-label`. Set
171
+ `activateOnFocus` so arrow keys select the tab.
144
172
 
145
- Keep collection filters in `FilterTabList`.
173
+ Keep collection filters in `FilterTabList`. It uses `TabsList variant="line"`
174
+ and adds the count badges.
146
175
 
147
- ### Patterns
176
+ ## Patterns
148
177
 
149
178
  - **Layout.** `layout/` holds the shell chrome, page headers, cards, and
150
179
  section titles. `DashboardChrome` owns the sticky header block and its one
151
180
  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.
181
+ - **Charts.** Put `role="img"` and the chart `aria-label` on the Recharts
182
+ chart, so the tooltip live region stays outside the image. Day and hour
183
+ charts compose the `charts/ActivityChart` axis, average line, and tooltip
184
+ helpers. They do not format UTC bucket keys inline.
158
185
  - **Conversation mobile geometry** is a hard ownership edge:
159
186
  - `layout/VisualViewportShell` owns the fixed shell, visualViewport CSS
160
187
  variables, and body lock.
@@ -166,16 +193,14 @@ Keep collection filters in `FilterTabList`.
166
193
  dock, and must not use `ComposerDock`. Do not add viewport height, offset, or
167
194
  bottom safe-area math in page modules.
168
195
 
169
- ### Features
170
-
171
196
  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.
197
+ share them. See [Transcript components](../conversations/README.md) for
198
+ transcript ownership.
174
199
 
175
200
  ## Rules
176
201
 
177
202
  - Prefer a named component on the second real use.
178
203
  - 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.
204
+ - Check new or changed shared components on a real page with mock data and in
205
+ the dashboard E2E screenshots.
181
206
  - One-off page glue may stay inline when extraction only adds indirection.
@@ -2,6 +2,12 @@ import type { Ref } from "react";
2
2
  import { Search, X } from "lucide-react";
3
3
 
4
4
  import { cn } from "../styles";
5
+ import {
6
+ InputGroup,
7
+ InputGroupAddon,
8
+ InputGroupButton,
9
+ InputGroupInput,
10
+ } from "./ui/input-group";
5
11
 
6
12
  type SearchInputSize = "compact" | "default";
7
13
 
@@ -15,23 +21,14 @@ export function SearchInput(props: {
15
21
  size?: SearchInputSize;
16
22
  value: string;
17
23
  }) {
18
- const size = props.size ?? "compact";
24
+ const size = sizeClass[props.size ?? "compact"];
19
25
  return (
20
- <div className={cn("relative min-w-0", props.className)}>
21
- <Search
22
- aria-hidden="true"
23
- className={cn(
24
- "pointer-events-none absolute top-1/2 -translate-y-1/2 text-dashboard-text-muted",
25
- searchIconClass[size],
26
- )}
27
- size={size === "compact" ? 13 : 14}
28
- strokeWidth={2.5}
29
- />
30
- <input
26
+ <InputGroup className={cn(size.group, props.className)}>
27
+ <InputGroupInput
31
28
  aria-label={props.label}
32
29
  className={cn(
33
- "w-full rounded-md border border-dashboard-border font-sans text-sm text-dashboard-text outline-none transition-colors placeholder:text-dashboard-text-muted/70 hover:border-dashboard-border-emphasis focus:border-dashboard-focus [&::-webkit-search-cancel-button]:appearance-none",
34
- searchInputClass[size],
30
+ "[&::-webkit-search-cancel-button]:appearance-none",
31
+ size.input,
35
32
  )}
36
33
  onChange={(event) => props.onChange(event.currentTarget.value)}
37
34
  placeholder={props.placeholder}
@@ -39,40 +36,53 @@ export function SearchInput(props: {
39
36
  type="search"
40
37
  value={props.value}
41
38
  />
42
- {props.value ? (
43
- <button
39
+ <InputGroupAddon className={size.icon}>
40
+ <Search aria-hidden="true" size={size.iconSize} strokeWidth={2.5} />
41
+ </InputGroupAddon>
42
+ {/* The clear button keeps its space while hidden, so the field width does not change. */}
43
+ <InputGroupAddon align="inline-end" className={size.clear}>
44
+ <InputGroupButton
44
45
  aria-label="Clear search"
45
- className={cn(
46
- "absolute top-1/2 grid size-6 -translate-y-1/2 cursor-pointer place-items-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
47
- clearButtonClass[size],
48
- )}
46
+ className={props.value ? undefined : "invisible"}
49
47
  onClick={(event) => {
50
48
  props.onChange("");
51
- // The button unmounts once the value is empty, so keep focus in the search box.
52
- event.currentTarget.parentElement?.querySelector("input")?.focus();
49
+ // The button hides once the value is empty, so keep focus in the search box.
50
+ event.currentTarget
51
+ .closest("[data-slot=input-group]")
52
+ ?.querySelector("input")
53
+ ?.focus();
53
54
  }}
54
- type="button"
55
55
  >
56
56
  <X aria-hidden="true" size={14} strokeWidth={2.5} />
57
- </button>
58
- ) : null}
59
- </div>
57
+ </InputGroupButton>
58
+ </InputGroupAddon>
59
+ </InputGroup>
60
60
  );
61
61
  }
62
62
 
63
- const searchIconClass: Record<SearchInputSize, string> = {
64
- compact: "left-2.5",
65
- default: "left-3",
66
- };
67
-
68
- const clearButtonClass: Record<SearchInputSize, string> = {
69
- compact: "right-1.5",
70
- default: "right-2",
71
- };
72
-
73
- const searchInputClass: Record<SearchInputSize, string> = {
74
- compact:
75
- "h-9 bg-dashboard-overlay-soft pr-8 pl-8 focus:ring-1 focus:ring-dashboard-focus/25",
76
- default:
77
- "h-10 bg-dashboard-overlay-soft pr-9 pl-9 focus:ring-1 focus:ring-dashboard-focus/25",
63
+ // Match the icon, text, and clear button offsets of the bordered input.
64
+ const sizeClass: Record<
65
+ SearchInputSize,
66
+ {
67
+ clear: string;
68
+ group: string;
69
+ icon: string;
70
+ iconSize: number;
71
+ input: string;
72
+ }
73
+ > = {
74
+ compact: {
75
+ clear: "pr-1.25",
76
+ group: "h-9",
77
+ icon: "pl-2.25",
78
+ iconSize: 13,
79
+ input: "pr-1 pl-2.5",
80
+ },
81
+ default: {
82
+ clear: "pr-1.75",
83
+ group: "h-10",
84
+ icon: "pl-2.75",
85
+ iconSize: 14,
86
+ input: "pr-1 pl-2.75",
87
+ },
78
88
  };
@@ -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}>
@@ -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,24 +1,26 @@
1
1
  import { JUNIOR_VERSION } from "@sentry/junior/version";
2
2
 
3
+ import { Alert } from "./ui/alert";
4
+ import { Button } from "./ui/button";
5
+
3
6
  /** Prompt the viewer to load the dashboard bundle that matches the server. */
4
7
  export function VersionDriftBanner(props: { serverVersion?: string }) {
5
8
  if (!dashboardVersionDrift(props.serverVersion)) return null;
6
9
 
7
10
  return (
8
- <div
11
+ <Alert
9
12
  aria-live="polite"
10
- className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-t border-dashboard-accent/15 bg-dashboard-accent/[0.07] px-3 py-1.5 text-center font-sans text-xs text-dashboard-accent-text/80"
13
+ className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 rounded-none border-0 border-t border-dashboard-accent/15 bg-dashboard-accent/[0.07] px-3 py-1.5 text-center font-sans text-xs text-dashboard-accent-text/80"
11
14
  role="status"
12
15
  >
13
16
  <span>The dashboard changed. Refresh to load the latest version.</span>
14
- <button
15
- className="cursor-pointer border-0 bg-transparent p-0 font-sans font-semibold text-dashboard-accent-text underline decoration-dashboard-accent/40 underline-offset-2 hover:text-dashboard-text-solid"
17
+ <Button
18
+ className="h-auto border-0 bg-transparent p-0 text-xs font-semibold leading-normal text-dashboard-accent-text underline decoration-dashboard-accent/40 underline-offset-2 hover:bg-transparent hover:text-dashboard-text-solid"
16
19
  onClick={() => window.location.reload()}
17
- type="button"
18
20
  >
19
21
  Refresh
20
- </button>
21
- </div>
22
+ </Button>
23
+ </Alert>
22
24
  );
23
25
  }
24
26