@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
@@ -1,20 +1,21 @@
1
+ import { Bar, BarChart, CartesianGrid } from "recharts";
1
2
  import {
2
3
  ActivityChartAverageLine,
3
- ActivityChartDateLabels,
4
- ActivityChartGrid,
5
4
  activityChartAverage,
6
- ActivityChartTooltip,
7
- ActivityTooltipRows,
8
- ChartSvg,
9
- createActivityChartLayout,
5
+ activityChartMargin,
6
+ ActivityChartTooltipContent,
7
+ ActivityChartXAxis,
8
+ ActivityChartYAxis,
10
9
  } from "./ActivityChart";
11
10
  import { ChartHeader } from "./ChartHeader";
12
11
  import type { TimeRangeDays } from "../controls/TimeRangeSelector";
13
12
  import { Card } from "../layout/Card";
13
+ import { ChartContainer, ChartTooltip } from "../ui/chart";
14
14
  import { formatActivityChartAverage, formatCompactNumber } from "../../format";
15
15
  import type { WorkspaceUsageDay } from "./workspaceUsage";
16
16
 
17
17
  const USAGE_COLOR = "var(--color-violet-400)";
18
+ const chartConfig = { count: { color: USAGE_COLOR, label: "Usage" } };
18
19
 
19
20
  /** Plot successful Workspace switches for one recipe over a trailing window. */
20
21
  export function WorkspaceUsageChart(props: {
@@ -22,12 +23,6 @@ export function WorkspaceUsageChart(props: {
22
23
  range: TimeRangeDays;
23
24
  workspaceName: string;
24
25
  }) {
25
- const layout = createActivityChartLayout(200);
26
- const step =
27
- props.days.length > 0
28
- ? layout.plotWidth / props.days.length
29
- : layout.plotWidth;
30
- const barWidth = Math.max(2, Math.min(13, step * 0.68));
31
26
  const values = props.days.map((day) => day.count);
32
27
  const maximum = Math.max(1, ...values);
33
28
  const average = activityChartAverage(values);
@@ -43,57 +38,42 @@ export function WorkspaceUsageChart(props: {
43
38
  totalLabel={`last ${props.range} days`}
44
39
  />
45
40
  <div className="relative px-2 py-3 sm:px-4 sm:py-4">
46
- <ChartSvg
47
- aria-label={`Usage for ${props.workspaceName} during the last ${props.range} days`}
48
- className="min-h-40 overflow-visible"
49
- layout={layout}
41
+ <ChartContainer
42
+ className="aspect-auto h-50 w-full"
43
+ config={chartConfig}
50
44
  >
51
- <ActivityChartGrid layout={layout} maximum={maximum} />
52
- {props.days.map((day, index) => {
53
- const x = layout.left + index * step + step / 2;
54
- const barHeight = (day.count / maximum) * layout.plotHeight;
55
- const renderedHeight = Math.max(day.count ? 2 : 0, barHeight);
56
- return (
57
- <ActivityChartTooltip
58
- key={day.date}
59
- content={<ActivityTooltipRows rows={[["usage", day.count]]} />}
60
- date={day.date}
61
- summary={`${day.count} switches`}
62
- >
63
- <g tabIndex={0}>
64
- <rect
65
- fill={USAGE_COLOR}
66
- height={renderedHeight}
67
- opacity={day.count ? 0.82 : 0.08}
68
- rx="2"
69
- width={barWidth}
70
- x={x - barWidth / 2}
71
- y={layout.top + layout.plotHeight - renderedHeight}
72
- />
73
- <rect
74
- fill="transparent"
75
- height={layout.plotHeight}
76
- width={step}
77
- x={layout.left + index * step}
78
- y={layout.top}
79
- />
80
- </g>
81
- </ActivityChartTooltip>
82
- );
83
- })}
84
- <ActivityChartAverageLine
85
- average={average}
86
- format={formatActivityChartAverage}
87
- layout={layout}
88
- maximum={maximum}
89
- stroke={USAGE_COLOR}
90
- />
91
- <ActivityChartDateLabels
92
- dates={props.days.map((day) => day.date)}
93
- layout={layout}
94
- xPosition={(index) => layout.left + index * step + step / 2}
95
- />
96
- </ChartSvg>
45
+ <BarChart
46
+ aria-label={`Usage for ${props.workspaceName} during the last ${props.range} days`}
47
+ data={props.days}
48
+ margin={activityChartMargin}
49
+ role="img"
50
+ >
51
+ <CartesianGrid strokeDasharray="3 5" vertical={false} />
52
+ <ActivityChartYAxis maximum={maximum} />
53
+ <ActivityChartXAxis keys={props.days.map((day) => day.date)} />
54
+ <ChartTooltip
55
+ content={
56
+ <ActivityChartTooltipContent
57
+ rows={(day: WorkspaceUsageDay) => [["usage", day.count]]}
58
+ />
59
+ }
60
+ />
61
+ <Bar
62
+ dataKey="count"
63
+ fill="var(--color-count)"
64
+ fillOpacity={0.82}
65
+ isAnimationActive={false}
66
+ maxBarSize={13}
67
+ minPointSize={(value) => (value ? 2 : 0)}
68
+ radius={2}
69
+ />
70
+ <ActivityChartAverageLine
71
+ average={average}
72
+ format={formatActivityChartAverage}
73
+ stroke={USAGE_COLOR}
74
+ />
75
+ </BarChart>
76
+ </ChartContainer>
97
77
  {!hasUsage ? (
98
78
  <div className="pointer-events-none absolute inset-0 grid place-items-center pt-12 font-sans text-xs text-dashboard-text-muted">
99
79
  No usage in this period.
@@ -1,4 +1,4 @@
1
- import { Select } from "../Select";
1
+ import { NativeSelect } from "../ui/native-select";
2
2
 
3
3
  export type DirectorySortOption = {
4
4
  label: string;
@@ -17,7 +17,7 @@ export function DirectorySortSelect(props: {
17
17
  <span className="flex h-full items-center border-r border-dashboard-border-subtle px-2 font-sans font-medium text-xs text-dashboard-text-muted">
18
18
  Sort
19
19
  </span>
20
- <Select
20
+ <NativeSelect
21
21
  aria-label={props.ariaLabel}
22
22
  variant="sort"
23
23
  value={props.value}
@@ -32,7 +32,7 @@ export function DirectorySortSelect(props: {
32
32
  {option.label}
33
33
  </option>
34
34
  ))}
35
- </Select>
35
+ </NativeSelect>
36
36
  </label>
37
37
  );
38
38
  }
@@ -1,4 +1,4 @@
1
- import { ToggleButton } from "../Button";
1
+ import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
2
2
 
3
3
  /**
4
4
  * Fixed dashboard reporting window.
@@ -124,9 +124,7 @@ function sumHoursIntoSixHours<T extends { date: string }>(args: {
124
124
  // Fill the trailing 28 six-hour buckets when enough hour rows exist.
125
125
  if (args.hours.length >= 24) {
126
126
  const lastHour = args.hours[args.hours.length - 1]?.date;
127
- const endMs = lastHour
128
- ? Date.parse(`${lastHour}:00:00.000Z`)
129
- : Date.now();
127
+ const endMs = lastHour ? Date.parse(`${lastHour}:00:00.000Z`) : Date.now();
130
128
  const end = new Date(endMs);
131
129
  end.setUTCMinutes(0, 0, 0);
132
130
  end.setUTCHours(Math.floor(end.getUTCHours() / 6) * 6, 0, 0, 0);
@@ -155,21 +153,18 @@ export function TimeRangeSelector(props: {
155
153
  value: TimeRangeDays;
156
154
  }) {
157
155
  return (
158
- <div
156
+ <ToggleGroup
159
157
  aria-label="Reporting period"
160
- className="flex items-center gap-1"
161
- role="group"
158
+ onValueChange={([next]) => {
159
+ if (next) props.onChange(Number(next) as TimeRangeDays);
160
+ }}
161
+ value={[String(props.value)]}
162
162
  >
163
163
  {(props.options ?? DEFAULT_OPTIONS).map((days) => (
164
- <ToggleButton
165
- key={days}
166
- onClick={() => props.onChange(days)}
167
- pressed={props.value === days}
168
- variant="segment"
169
- >
164
+ <ToggleGroupItem key={days} value={String(days)}>
170
165
  {timeRangeLabel(days)}
171
- </ToggleButton>
166
+ </ToggleGroupItem>
172
167
  ))}
173
- </div>
168
+ </ToggleGroup>
174
169
  );
175
170
  }
@@ -1,13 +1,11 @@
1
1
  import { ArrowLeft, Menu, X } from "lucide-react";
2
- import { useEffect, useRef, type ReactNode } from "react";
2
+ import { useEffect, useRef, type ReactNode, type RefObject } from "react";
3
3
  import { Link, NavLink } from "react-router";
4
4
 
5
5
  import { getDashboardAgentName } from "../../agentName";
6
- import {
7
- acquireBodyScrollLock,
8
- releaseBodyScrollLock,
9
- } from "../../bodyScrollLock";
10
6
  import { JuniorLogo } from "../JuniorLogo";
7
+ import { Sheet, SheetClose, SheetContent } from "../ui/sheet";
8
+ import { useSheet } from "../ui/useSheet";
11
9
  import {
12
10
  MobileHeaderActionsSlot,
13
11
  MobileHeaderLiveSlot,
@@ -22,15 +20,6 @@ export type DashboardHeaderNavItem = {
22
20
  to: string;
23
21
  };
24
22
 
25
- const focusableSelector = [
26
- "a[href]",
27
- "button:not([disabled])",
28
- "input:not([disabled]):not([type='hidden'])",
29
- "select:not([disabled])",
30
- "textarea:not([disabled])",
31
- "[tabindex]:not([tabindex='-1'])",
32
- ].join(",");
33
-
34
23
  /** Render the primary dashboard shell header and optional mobile nav sheet. */
35
24
  export function DashboardHeader(props: {
36
25
  /** Back target used on mobile conversation detail. */
@@ -50,9 +39,7 @@ export function DashboardHeader(props: {
50
39
  version?: string;
51
40
  workspaceActive: boolean;
52
41
  }) {
53
- const sheetRef = useRef<HTMLDivElement>(null);
54
- const openButtonRef = useRef<HTMLButtonElement>(null);
55
- const previousFocusRef = useRef<HTMLElement | undefined>(undefined);
42
+ const closeRef = useRef<HTMLButtonElement>(null);
56
43
  const onOpenChangeRef = useRef(props.onMobileNavigationOpenChange);
57
44
  onOpenChangeRef.current = props.onMobileNavigationOpenChange;
58
45
  // Conversation chrome is path-driven. Undefined title keeps the loading
@@ -90,56 +77,7 @@ export function DashboardHeader(props: {
90
77
  };
91
78
  closeWhenDesktop();
92
79
  mobile.addEventListener("change", closeWhenDesktop);
93
-
94
- previousFocusRef.current =
95
- document.activeElement instanceof HTMLElement
96
- ? document.activeElement
97
- : (openButtonRef.current ?? undefined);
98
- acquireBodyScrollLock();
99
- const focusFrame = requestAnimationFrame(() => {
100
- sheetRef.current
101
- ?.querySelector<HTMLElement>("[data-mobile-nav-close]")
102
- ?.focus();
103
- });
104
-
105
- const onKeyDown = (event: KeyboardEvent) => {
106
- if (event.key !== "Tab") return;
107
- const sheet = sheetRef.current;
108
- const focusable = Array.from(
109
- sheet?.querySelectorAll<HTMLElement>(focusableSelector) ?? [],
110
- ).filter(
111
- (element) =>
112
- element.tabIndex >= 0 && element.getClientRects().length > 0,
113
- );
114
- const first = focusable.at(0);
115
- const last = focusable.at(-1);
116
- if (!sheet || !first || !last) {
117
- event.preventDefault();
118
- return;
119
- }
120
- const active = document.activeElement;
121
- if (event.shiftKey && (active === first || !sheet.contains(active))) {
122
- event.preventDefault();
123
- last.focus();
124
- } else if (
125
- !event.shiftKey &&
126
- (active === last || !sheet.contains(active))
127
- ) {
128
- event.preventDefault();
129
- first.focus();
130
- }
131
- };
132
-
133
- window.addEventListener("keydown", onKeyDown);
134
- return () => {
135
- cancelAnimationFrame(focusFrame);
136
- mobile.removeEventListener("change", closeWhenDesktop);
137
- window.removeEventListener("keydown", onKeyDown);
138
- releaseBodyScrollLock();
139
- const previousFocus = previousFocusRef.current;
140
- previousFocusRef.current = undefined;
141
- if (previousFocus?.isConnected) previousFocus.focus();
142
- };
80
+ return () => mobile.removeEventListener("change", closeWhenDesktop);
143
81
  }, [props.mobileNavigationOpen]);
144
82
 
145
83
  return (
@@ -168,7 +106,6 @@ export function DashboardHeader(props: {
168
106
  aria-label="Open navigation"
169
107
  className="grid size-10 cursor-pointer place-items-center rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70 md:hidden"
170
108
  onClick={() => props.onMobileNavigationOpenChange(true)}
171
- ref={openButtonRef}
172
109
  tabIndex={props.mobileNavigationOpen ? -1 : undefined}
173
110
  type="button"
174
111
  >
@@ -225,22 +162,9 @@ export function DashboardHeader(props: {
225
162
  ) : null}
226
163
  </div>
227
164
  {props.mobileNavigationOpen ? (
228
- <div
229
- aria-label="Navigation"
230
- aria-modal="true"
231
- className="fixed inset-0 z-50 flex flex-col bg-dashboard-bg-elevated md:hidden"
232
- id="mobile-navigation"
233
- onClick={(event) => {
234
- // Close on route taps and sheet actions such as Log out.
235
- if (
236
- event.target instanceof Element &&
237
- event.target.closest("a[href], [data-mobile-nav-dismiss]")
238
- ) {
239
- onOpenChangeRef.current(false);
240
- }
241
- }}
242
- ref={sheetRef}
243
- role="dialog"
165
+ <MobileNavigationSheet
166
+ closeRef={closeRef}
167
+ onClose={() => onOpenChangeRef.current(false)}
244
168
  >
245
169
  <div className="flex items-center justify-between gap-3 border-b border-dashboard-border-subtle bg-dashboard-surface-raised px-3 py-2 pt-[max(0.5rem,env(safe-area-inset-top))]">
246
170
  <Link
@@ -250,15 +174,13 @@ export function DashboardHeader(props: {
250
174
  >
251
175
  {getDashboardAgentName()}
252
176
  </Link>
253
- <button
177
+ <SheetClose
254
178
  aria-label="Close navigation"
255
179
  className="grid size-10 cursor-pointer place-items-center rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70"
256
- data-mobile-nav-close
257
- onClick={() => props.onMobileNavigationOpenChange(false)}
258
- type="button"
180
+ ref={closeRef}
259
181
  >
260
182
  <X aria-hidden="true" size={20} strokeWidth={2} />
261
- </button>
183
+ </SheetClose>
262
184
  </div>
263
185
  <div className="flex min-h-0 flex-1 flex-col">
264
186
  <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">
@@ -307,8 +229,40 @@ export function DashboardHeader(props: {
307
229
  </div>
308
230
  ) : null}
309
231
  </div>
310
- </div>
232
+ </MobileNavigationSheet>
311
233
  ) : null}
312
234
  </header>
313
235
  );
314
236
  }
237
+
238
+ /** Show the mobile navigation in a full-screen shared sheet. */
239
+ function MobileNavigationSheet(props: {
240
+ children: ReactNode;
241
+ closeRef: RefObject<HTMLButtonElement | null>;
242
+ onClose(): void;
243
+ }) {
244
+ const sheet = useSheet(props.onClose, props.closeRef);
245
+ return (
246
+ <Sheet {...sheet.rootProps}>
247
+ <SheetContent
248
+ {...sheet.contentProps}
249
+ aria-label="Navigation"
250
+ className="inset-0 gap-0 bg-dashboard-bg-elevated outline-none data-[side=left]:w-full data-[side=left]:border-r-0 data-[side=left]:sm:max-w-none md:hidden"
251
+ id="mobile-navigation"
252
+ onClick={(event) => {
253
+ // Close on route taps and sheet actions such as Log out.
254
+ if (
255
+ event.target instanceof Element &&
256
+ event.target.closest("a[href], [data-mobile-nav-dismiss]")
257
+ ) {
258
+ props.onClose();
259
+ }
260
+ }}
261
+ showCloseButton={false}
262
+ side="left"
263
+ >
264
+ {props.children}
265
+ </SheetContent>
266
+ </Sheet>
267
+ );
268
+ }
@@ -22,6 +22,7 @@ export type SecondaryNavigationItem = SegmentedNavItem;
22
22
  */
23
23
  export function SecondaryNavigation(props: {
24
24
  ariaLabel: string;
25
+ className?: string;
25
26
  children: ReactNode;
26
27
  items: SecondaryNavigationItem[];
27
28
  }) {
@@ -34,7 +35,7 @@ export function SecondaryNavigation(props: {
34
35
  );
35
36
 
36
37
  return (
37
- <div className="min-w-0">
38
+ <div className={cn("min-w-0", props.className)}>
38
39
  <SecondaryNavigationPortal
39
40
  desktop={
40
41
  <div className={cn(dashboardContainerClass, "min-w-0 px-4 md:px-8")}>
@@ -6,13 +6,13 @@ import { cn } from "../../styles";
6
6
  /**
7
7
  * Own the dashboard visual-viewport shell.
8
8
  *
9
- * Only this component may pin the conversation workspace to visualViewport
9
+ * Only this component may pin pages with separate scroll panes to visualViewport
10
10
  * geometry and write the related CSS variables / body scroll lock.
11
11
  */
12
12
  export function VisualViewportShell(props: {
13
13
  children: ReactNode;
14
14
  className?: string;
15
- /** Pin the shell to the mobile visual viewport (conversation routes). */
15
+ /** Pin pages with separate scroll panes to the mobile visual viewport. */
16
16
  enabled: boolean;
17
17
  style?: CSSProperties;
18
18
  }) {
@@ -0,0 +1,77 @@
1
+ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2
+ import { cn } from "@dashboard/lib/utils";
3
+ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
4
+
5
+ /** Group sections with Base UI disclosure and keyboard behavior. */
6
+ function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
7
+ return (
8
+ <AccordionPrimitive.Root
9
+ data-slot="accordion"
10
+ className={cn("flex w-full flex-col", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ /** Keep one section’s trigger and panel state together. */
17
+ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
18
+ return (
19
+ <AccordionPrimitive.Item
20
+ data-slot="accordion-item"
21
+ className={cn(
22
+ "not-last:border-b not-last:border-dashboard-border-subtle",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ );
28
+ }
29
+
30
+ /** Render the section heading and its expand or collapse control. */
31
+ function AccordionTrigger({
32
+ className,
33
+ children,
34
+ ...props
35
+ }: AccordionPrimitive.Trigger.Props) {
36
+ return (
37
+ <AccordionPrimitive.Header className="flex">
38
+ <AccordionPrimitive.Trigger
39
+ data-slot="accordion-trigger"
40
+ className={cn(
41
+ "group/accordion-trigger relative flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-3 rounded-sm py-2.5 text-left font-sans text-sm font-medium text-dashboard-text transition-colors hover:text-dashboard-text-solid outline-none focus-visible:outline-2 focus-visible:outline-dashboard-focus aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-dashboard-text-muted",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <ChevronDownIcon
48
+ data-slot="accordion-trigger-icon"
49
+ className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
50
+ />
51
+ <ChevronUpIcon
52
+ data-slot="accordion-trigger-icon"
53
+ className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
54
+ />
55
+ </AccordionPrimitive.Trigger>
56
+ </AccordionPrimitive.Header>
57
+ );
58
+ }
59
+
60
+ /** Reveal section content with measured height and reduced-motion support. */
61
+ function AccordionContent({
62
+ className,
63
+ children,
64
+ ...props
65
+ }: AccordionPrimitive.Panel.Props) {
66
+ return (
67
+ <AccordionPrimitive.Panel
68
+ data-slot="accordion-content"
69
+ className="h-(--accordion-panel-height) overflow-hidden text-sm transition-[height] duration-180 ease-out data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none"
70
+ {...props}
71
+ >
72
+ <div className={cn("pt-0 pb-2.5", className)}>{children}</div>
73
+ </AccordionPrimitive.Panel>
74
+ );
75
+ }
76
+
77
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,89 @@
1
+ import type { ComponentProps } from "react";
2
+ import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
3
+ import { cn } from "@dashboard/lib/utils";
4
+ import { Button } from "./button";
5
+
6
+ /** Require an explicit response before a destructive action. */
7
+ export function AlertDialog(props: AlertDialogPrimitive.Root.Props) {
8
+ return <AlertDialogPrimitive.Root {...props} />;
9
+ }
10
+
11
+ /** Show a named confirmation above the page with modal focus handling. */
12
+ export function AlertDialogContent({
13
+ className,
14
+ ...props
15
+ }: AlertDialogPrimitive.Popup.Props) {
16
+ return (
17
+ <AlertDialogPrimitive.Portal>
18
+ <AlertDialogPrimitive.Backdrop className="fixed inset-0 z-50 bg-dashboard-overlay transition-opacity duration-180 motion-reduce:transition-none data-ending-style:opacity-0 data-starting-style:opacity-0" />
19
+ <AlertDialogPrimitive.Popup
20
+ data-dashboard-modal=""
21
+ data-slot="alert-dialog-content"
22
+ className={cn(
23
+ "fixed top-1/2 left-1/2 z-50 grid grid-cols-1 max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-xl border border-dashboard-border-emphasis bg-dashboard-surface-raised p-5 text-dashboard-text shadow-lg outline-none transition-[opacity,scale] duration-180 motion-reduce:transition-none data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:scale-95 data-starting-style:scale-95",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ </AlertDialogPrimitive.Portal>
29
+ );
30
+ }
31
+
32
+ /** Name the action that needs confirmation. */
33
+ export function AlertDialogTitle({
34
+ className,
35
+ ...props
36
+ }: AlertDialogPrimitive.Title.Props) {
37
+ return (
38
+ <AlertDialogPrimitive.Title
39
+ className={cn("m-0 font-display text-lg font-medium", className)}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ /** Describe what the action changes before the user confirms it. */
46
+ export function AlertDialogDescription({
47
+ className,
48
+ ...props
49
+ }: AlertDialogPrimitive.Description.Props) {
50
+ return (
51
+ <AlertDialogPrimitive.Description
52
+ className={cn(
53
+ "m-0 break-words text-sm leading-relaxed text-dashboard-text-muted",
54
+ className,
55
+ )}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ /** Keep confirmation controls together and reachable on small screens. */
62
+ export function AlertDialogFooter({
63
+ className,
64
+ ...props
65
+ }: ComponentProps<"div">) {
66
+ return (
67
+ <div
68
+ className={cn("flex flex-wrap justify-end gap-2", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ /** Run the confirmed action; its owner decides when the dialog can close. */
75
+ export function AlertDialogAction(props: ComponentProps<typeof Button>) {
76
+ return (
77
+ <Button data-slot="alert-dialog-action" className="min-h-11" {...props} />
78
+ );
79
+ }
80
+
81
+ /** Cancel through Base UI so focus returns to the source control. */
82
+ export function AlertDialogCancel(props: AlertDialogPrimitive.Close.Props) {
83
+ return (
84
+ <AlertDialogPrimitive.Close
85
+ render={<Button className="min-h-11" />}
86
+ {...props}
87
+ />
88
+ );
89
+ }
@@ -0,0 +1,64 @@
1
+ import type { ComponentProps } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@dashboard/lib/utils";
4
+
5
+ const alertVariants = cva(
6
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "border-dashboard-border-strong bg-dashboard-surface-raised text-dashboard-text",
12
+ destructive:
13
+ "border-rose-300/25 bg-dashboard-surface-raised text-rose-50 *:data-[slot=alert-description]:text-rose-100/70 *:[svg]:text-current",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ );
21
+
22
+ /** Show a message in a live region. Pass `role="status"` for polite updates. */
23
+ function Alert({
24
+ className,
25
+ variant,
26
+ ...props
27
+ }: ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
28
+ return (
29
+ <div
30
+ data-slot="alert"
31
+ role="alert"
32
+ className={cn(alertVariants({ variant }), className)}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+
38
+ function AlertTitle({ className, ...props }: ComponentProps<"div">) {
39
+ return (
40
+ <div
41
+ data-slot="alert-title"
42
+ className={cn(
43
+ "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-dashboard-text",
44
+ className,
45
+ )}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function AlertDescription({ className, ...props }: ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="alert-description"
55
+ className={cn(
56
+ "text-sm text-balance text-dashboard-text-muted md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-dashboard-text [&_p:not(:last-child)]:mb-4",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ export { Alert, AlertTitle, AlertDescription };