@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
@@ -0,0 +1,94 @@
1
+ import type { ConversationWatchesReport } from "@sentry/junior/api/schema";
2
+ import {
3
+ DASHBOARD_QA_CONVERSATION_ID,
4
+ readMockConversationDetail,
5
+ } from "./fixtures";
6
+
7
+ const HOUR_MS = 60 * 60_000;
8
+ const DAY_MS = 24 * HOUR_MS;
9
+
10
+ // Watch times are relative to the real clock, not the fixed fixture time, so
11
+ // time-left labels stay readable.
12
+ function iso(offsetMs: number): string {
13
+ return new Date(Date.now() + offsetMs).toISOString();
14
+ }
15
+
16
+ /**
17
+ * Return the active Watches of one mock Conversation. The dashboard QA
18
+ * Conversation has one Watch of each common shape: a pull request with a
19
+ * match filter, a deployment, a timer, a redacted Watch, and one that a child
20
+ * agent created.
21
+ */
22
+ export function readMockConversationWatches(
23
+ conversationId: string,
24
+ ): ConversationWatchesReport | undefined {
25
+ if (!readMockConversationDetail(conversationId)) return undefined;
26
+ if (conversationId !== DASHBOARD_QA_CONVERSATION_ID) {
27
+ return { conversationId, watches: [] };
28
+ }
29
+ return {
30
+ conversationId,
31
+ watches: [
32
+ {
33
+ createdAt: iso(-3 * HOUR_MS),
34
+ events: [
35
+ "pull_request.checks.failed",
36
+ "pull_request.review.changes_requested",
37
+ "pull_request.merged",
38
+ ],
39
+ expiresAt: iso(13 * DAY_MS),
40
+ id: "watch-pr",
41
+ intent:
42
+ "Fix failing checks and address requested changes on the dashboard PR.",
43
+ label: "getsentry/junior#2383",
44
+ namespace: "github",
45
+ resourceType: "pull_request",
46
+ },
47
+ {
48
+ createdAt: iso(-2 * HOUR_MS),
49
+ events: ["deployment.failed", "deployment.succeeded"],
50
+ expiresAt: iso(20 * HOUR_MS),
51
+ id: "watch-deploy",
52
+ intent: "Report the preview URL, or the build error if it fails.",
53
+ label: "junior-dashboard preview",
54
+ match: { target: "preview" },
55
+ namespace: "vercel",
56
+ resourceType: "deployment",
57
+ },
58
+ {
59
+ createdAt: iso(-30 * 60_000),
60
+ events: ["timer.fired"],
61
+ expiresAt: iso(2 * HOUR_MS + 5 * 60_000),
62
+ firesAt: iso(2 * HOUR_MS),
63
+ id: "watch-timer",
64
+ intent: "Check whether CI recovered after the retry.",
65
+ label: "Recheck CI",
66
+ namespace: "junior",
67
+ resourceType: "timer",
68
+ },
69
+ {
70
+ childAgent: {
71
+ conversationId: "junior:internal:dashboard-qa:advisor-review",
72
+ name: "reviewer",
73
+ },
74
+ createdAt: iso(-20 * 60_000),
75
+ events: ["pull_request.review_comment.created"],
76
+ expiresAt: iso(6 * DAY_MS),
77
+ id: "watch-child",
78
+ intent: "Reply to new review comments on the PR.",
79
+ label: "getsentry/junior#2351",
80
+ namespace: "github",
81
+ resourceType: "pull_request",
82
+ },
83
+ {
84
+ createdAt: iso(-10 * 60_000),
85
+ events: ["message.posted"],
86
+ expiresAt: iso(3 * DAY_MS),
87
+ id: "watch-redacted",
88
+ namespace: "slack",
89
+ redacted: true,
90
+ resourceType: "channel",
91
+ },
92
+ ],
93
+ };
94
+ }
package/src/shell.ts CHANGED
@@ -117,9 +117,8 @@ function dashboardStartUrl(basePath: string): string {
117
117
  /** List the browser page paths served by the authenticated dashboard shell. */
118
118
  export function dashboardPagePaths(
119
119
  basePath: string,
120
- options: { componentGallery?: boolean } = {},
121
120
  ): Array<{ nested?: boolean; path: string }> {
122
- const paths: Array<{ nested?: boolean; path: string }> = [
121
+ return [
123
122
  { path: basePath },
124
123
  {
125
124
  path: basePath === "/" ? "/admin" : `${basePath}/admin`,
@@ -165,13 +164,6 @@ export function dashboardPagePaths(
165
164
  path: basePath === "/" ? "/plugins" : `${basePath}/plugins`,
166
165
  },
167
166
  ];
168
- if (options.componentGallery) {
169
- paths.push({
170
- nested: true,
171
- path: basePath === "/" ? "/dev" : `${basePath}/dev`,
172
- });
173
- }
174
- return paths;
175
167
  }
176
168
 
177
169
  /** Serve the installable web app manifest for the dashboard shell. */
@@ -286,6 +278,9 @@ export function renderDashboard(basePath: string, agentName: string): Response {
286
278
  "</pre></section></main>";
287
279
  };
288
280
  window.addEventListener("error", function (event) {
281
+ // Browsers report a ResizeObserver loop as an error event with no
282
+ // error object. Layout continues, so it must not replace the page.
283
+ if (!event.error && /^ResizeObserver loop/.test(event.message)) return;
289
284
  window.__JUNIOR_DASHBOARD_SHOW_ERROR__(event.error || event.message);
290
285
  });
291
286
  window.addEventListener("unhandledrejection", function (event) {
package/src/tailwind.css CHANGED
@@ -338,16 +338,6 @@
338
338
  }
339
339
  }
340
340
 
341
- @keyframes junior-text-shimmer {
342
- 0% {
343
- background-position: 200% 50%;
344
- }
345
-
346
- 100% {
347
- background-position: -200% 50%;
348
- }
349
- }
350
-
351
341
  @keyframes junior-active-indicator {
352
342
  0%,
353
343
  100% {
@@ -1,118 +0,0 @@
1
- import type { ButtonHTMLAttributes, Ref } from "react";
2
- import { Link, type LinkProps } from "react-router";
3
-
4
- import { cn } from "../styles";
5
-
6
- type ButtonSize = "default" | "icon";
7
- type ButtonTone = "default" | "danger" | "primary";
8
- type ToggleButtonVariant = "pill" | "segment" | "text";
9
-
10
- export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
11
- ref?: Ref<HTMLButtonElement>;
12
- size?: ButtonSize;
13
- tone?: ButtonTone;
14
- };
15
-
16
- export type ToggleButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
17
- pressed: boolean;
18
- variant: ToggleButtonVariant;
19
- };
20
-
21
- const buttonClassName =
22
- "inline-flex h-9 max-w-full items-center justify-center gap-2 rounded-md border border-dashboard-border-emphasis bg-dashboard-surface-raised px-3 font-sans text-sm font-medium leading-none text-dashboard-text transition-colors hover:border-dashboard-border-heavy hover:bg-dashboard-surface-hover hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus";
23
-
24
- const dangerButtonClassName =
25
- "inline-flex h-9 max-w-full items-center justify-center gap-2 rounded-md border border-dashboard-border-strong bg-transparent px-3 font-sans text-sm font-medium leading-none text-dashboard-text-muted transition-colors hover:border-rose-300/40 hover:text-rose-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-300/55";
26
-
27
- /** Render the dashboard's standard bordered command button surface. */
28
- export function Button({
29
- className,
30
- size = "default",
31
- tone = "default",
32
- type = "button",
33
- ...props
34
- }: ButtonProps) {
35
- const isDanger = tone === "danger";
36
- return (
37
- <button
38
- {...props}
39
- className={cn(
40
- "font-sans transition-colors disabled:cursor-not-allowed disabled:opacity-50",
41
- size === "icon"
42
- ? cn(
43
- "grid size-9 place-items-center rounded-md border-0 bg-transparent p-0 focus-visible:outline-2 focus-visible:outline-offset-1 disabled:hover:!bg-transparent",
44
- isDanger
45
- ? "text-dashboard-text-muted hover:!bg-rose-300/10 hover:!text-rose-300 focus-visible:outline-rose-300/55 disabled:hover:!text-dashboard-text-muted"
46
- : "text-dashboard-text-muted hover:!bg-dashboard-fill-strong hover:!text-dashboard-text focus-visible:outline-dashboard-focus disabled:hover:!text-dashboard-text-muted",
47
- )
48
- : tone === "primary"
49
- ? "inline-flex h-9 items-center justify-center gap-2 rounded-md border border-dashboard-text-solid bg-dashboard-text-solid px-3 font-sans text-sm font-medium leading-none text-dashboard-text-inverse hover:border-dashboard-text-subtle hover:bg-dashboard-text-subtle disabled:hover:border-dashboard-text-solid disabled:hover:bg-dashboard-text-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
50
- : isDanger
51
- ? cn(
52
- dangerButtonClassName,
53
- "disabled:hover:border-dashboard-border-strong disabled:hover:text-dashboard-text-muted",
54
- )
55
- : cn(
56
- buttonClassName,
57
- "disabled:hover:border-dashboard-border-emphasis disabled:hover:bg-dashboard-surface-raised disabled:hover:text-dashboard-text",
58
- ),
59
- props.disabled ? "" : "cursor-pointer",
60
- className,
61
- )}
62
- type={type}
63
- />
64
- );
65
- }
66
-
67
- /** Render a dashboard navigation link with the standard button surface. */
68
- export function ButtonLink({ className, ...props }: LinkProps) {
69
- return (
70
- <Link
71
- {...props}
72
- className={cn(buttonClassName, "no-underline", className)}
73
- />
74
- );
75
- }
76
-
77
- /** Render a dashboard toggle button with a consistent pressed-state contract. */
78
- export function ToggleButton({
79
- className,
80
- pressed,
81
- type = "button",
82
- variant,
83
- ...props
84
- }: ToggleButtonProps) {
85
- return (
86
- <button
87
- {...props}
88
- aria-pressed={pressed}
89
- className={cn(
90
- toggleButtonBase[variant],
91
- pressed ? toggleButtonPressed[variant] : toggleButtonIdle[variant],
92
- className,
93
- )}
94
- type={type}
95
- />
96
- );
97
- }
98
-
99
- const toggleButtonBase: Record<ToggleButtonVariant, string> = {
100
- pill: "cursor-pointer rounded-md border px-2 py-1 font-sans text-xs font-medium leading-tight transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus",
101
- segment:
102
- "inline-flex h-7 min-w-10 cursor-pointer items-center justify-center rounded-md border px-2 font-sans text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus",
103
- text: "cursor-pointer rounded-sm border-0 bg-transparent px-1.5 py-1 font-sans font-medium underline-offset-4 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus",
104
- };
105
-
106
- const toggleButtonPressed: Record<ToggleButtonVariant, string> = {
107
- pill: "border-dashboard-border-heavy bg-dashboard-text-solid text-dashboard-text-inverse",
108
- segment:
109
- "border-dashboard-border-interactive bg-dashboard-surface-active text-dashboard-text",
110
- text: "text-dashboard-text underline decoration-dashboard-text-solid",
111
- };
112
-
113
- const toggleButtonIdle: Record<ToggleButtonVariant, string> = {
114
- pill: "border-dashboard-border-strong bg-dashboard-surface-raised text-dashboard-text-muted hover:border-dashboard-border-interactive hover:bg-dashboard-surface-hover hover:text-dashboard-text",
115
- segment:
116
- "border-transparent bg-transparent text-dashboard-text-muted hover:bg-dashboard-fill-hover hover:text-dashboard-text",
117
- text: "text-dashboard-text-muted hover:text-dashboard-text",
118
- };
@@ -1,53 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- import { cn } from "../styles";
4
-
5
- export type FieldSize = "compact" | "default";
6
-
7
- const labelClass: Record<FieldSize, string> = {
8
- compact: "font-sans font-medium text-xs text-dashboard-text-muted",
9
- default: "text-sm font-semibold text-dashboard-text",
10
- };
11
-
12
- const stackClass: Record<FieldSize, string> = {
13
- compact: "grid gap-1.5",
14
- default: "grid gap-2",
15
- };
16
-
17
- /** Label a control with optional help text using the shared form stack. */
18
- export function Field(props: {
19
- children: ReactNode;
20
- className?: string;
21
- help?: ReactNode;
22
- error?: string;
23
- htmlFor?: string;
24
- label: ReactNode;
25
- size?: FieldSize;
26
- }) {
27
- const size = props.size ?? "default";
28
- return (
29
- <div className={cn(stackClass[size], props.className)}>
30
- <label className={labelClass[size]} htmlFor={props.htmlFor}>
31
- {props.label}
32
- </label>
33
- {props.children}
34
- {props.error ? (
35
- <p
36
- id={props.htmlFor ? `${props.htmlFor}-error` : undefined}
37
- role="alert"
38
- className="m-0 text-xs leading-relaxed text-rose-300"
39
- >
40
- {props.error}
41
- </p>
42
- ) : null}
43
- {props.help ? (
44
- <p
45
- id={props.htmlFor ? `${props.htmlFor}-help` : undefined}
46
- className="m-0 text-xs leading-relaxed text-dashboard-text-muted sm:text-sm"
47
- >
48
- {props.help}
49
- </p>
50
- ) : null}
51
- </div>
52
- );
53
- }
@@ -1,119 +0,0 @@
1
- import { useId, type ReactNode } from "react";
2
-
3
- import { cn } from "../styles";
4
-
5
- /** Style the border and fill of the segmented pill track. */
6
- const segmentedTabsFrameClass =
7
- "rounded-lg border border-dashboard-border bg-dashboard-surface-panel";
8
-
9
- /** Style the spacing inside the segmented pill track. */
10
- const segmentedTabsInsetClass = "gap-1 p-1";
11
-
12
- /** Style one tab or link in the segmented pill track. */
13
- export function segmentedTabClass(selected: boolean): string {
14
- return cn(
15
- "min-h-9 cursor-pointer rounded-md border-0 px-1.5 py-2 font-sans text-xs font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus",
16
- selected
17
- ? "bg-dashboard-fill-strong text-dashboard-text"
18
- : "bg-transparent text-dashboard-text-muted hover:bg-dashboard-fill-faint hover:text-dashboard-text",
19
- );
20
- }
21
-
22
- /** Switch between related panels. Tabs share the row equally on wide screens. */
23
- export function SegmentedTabs<const Value extends string>(props: {
24
- children: ReactNode;
25
- label: string;
26
- /** Fill the frame with fixed tabs and padded, scrolling content. */
27
- scroll?: boolean;
28
- /** `count` is the number of entries in the panel. Zero and unknown counts are hidden. */
29
- items: readonly { count?: number; label: string; value: Value }[];
30
- onChange(value: NoInfer<Value>): void;
31
- value: NoInfer<Value>;
32
- }) {
33
- const id = useId();
34
-
35
- return (
36
- <div
37
- className={cn(
38
- "grid min-w-0",
39
- props.scroll
40
- ? "h-full min-h-0 grid-rows-[auto_minmax(0,1fr)]"
41
- : "gap-4",
42
- )}
43
- >
44
- <div
45
- aria-label={props.label}
46
- className={cn(
47
- "flex min-w-0",
48
- segmentedTabsFrameClass,
49
- segmentedTabsInsetClass,
50
- props.scroll && "m-4",
51
- )}
52
- role="tablist"
53
- >
54
- {props.items.map((item, index) => (
55
- <button
56
- aria-controls={`${id}-panel`}
57
- aria-selected={props.value === item.value}
58
- className={cn(
59
- "inline-flex min-w-0 flex-auto items-center justify-center gap-1.5 sm:flex-1",
60
- segmentedTabClass(props.value === item.value),
61
- )}
62
- id={`${id}-${item.value}`}
63
- key={item.value}
64
- onClick={() => props.onChange(item.value)}
65
- onKeyDown={(event) => {
66
- let nextIndex: number;
67
- const count = props.items.length;
68
- switch (event.key) {
69
- case "ArrowLeft":
70
- nextIndex = (index + count - 1) % count;
71
- break;
72
- case "ArrowRight":
73
- nextIndex = (index + 1) % count;
74
- break;
75
- case "Home":
76
- nextIndex = 0;
77
- break;
78
- case "End":
79
- nextIndex = count - 1;
80
- break;
81
- default:
82
- return;
83
- }
84
- event.preventDefault();
85
- props.onChange(props.items[nextIndex]!.value);
86
- event.currentTarget.parentElement
87
- ?.querySelectorAll<HTMLButtonElement>('[role="tab"]')
88
- [nextIndex]?.focus();
89
- }}
90
- role="tab"
91
- tabIndex={props.value === item.value ? 0 : -1}
92
- type="button"
93
- >
94
- <span className="truncate">{item.label}</span>
95
- {item.count ? (
96
- <span className="min-w-5 shrink-0 rounded-full bg-dashboard-fill-strong px-1.5 text-center text-xs font-semibold leading-5 tabular-nums">
97
- {item.count.toLocaleString("en-US")}
98
- </span>
99
- ) : null}
100
- </button>
101
- ))}
102
- </div>
103
- <div
104
- aria-labelledby={`${id}-${props.value}`}
105
- className={cn(
106
- "min-w-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus",
107
- props.scroll
108
- ? "min-h-0 overflow-y-auto overscroll-contain px-4 pb-[max(1rem,env(safe-area-inset-bottom))] focus-visible:-outline-offset-2"
109
- : "rounded-md",
110
- )}
111
- id={`${id}-panel`}
112
- role="tabpanel"
113
- tabIndex={0}
114
- >
115
- {props.children}
116
- </div>
117
- </div>
118
- );
119
- }
@@ -1,52 +0,0 @@
1
- import type { SelectHTMLAttributes } from "react";
2
- import { ChevronDown } from "lucide-react";
3
- import { cn } from "../styles";
4
-
5
- const selectClassName = {
6
- form: "min-h-11 rounded-md border border-dashboard-border-emphasis bg-dashboard-ink pl-3 py-2 text-base text-dashboard-text focus:border-dashboard-focus focus:outline-none aria-invalid:border-rose-300 sm:min-h-10 sm:text-sm",
7
- filter:
8
- "h-9 rounded-lg border border-dashboard-border-emphasis bg-dashboard-control pl-2 text-sm text-dashboard-text transition-colors hover:border-dashboard-border-interactive focus:border-dashboard-focus focus:outline-none focus:ring-1 focus:ring-dashboard-focus/25",
9
- sort: "h-full bg-dashboard-control pl-2 font-sans text-xs text-dashboard-text-muted outline-none",
10
- };
11
-
12
- /** Keep native choices and position their arrow consistently across the dashboard. */
13
- export function Select({
14
- className,
15
- variant = "form",
16
- ...props
17
- }: SelectHTMLAttributes<HTMLSelectElement> & {
18
- variant?: keyof typeof selectClassName;
19
- }) {
20
- const showArrow = !props.multiple && (props.size ?? 1) <= 1;
21
-
22
- return (
23
- <span
24
- className={cn(
25
- "relative block w-full min-w-0",
26
- variant === "sort" && "h-full",
27
- )}
28
- >
29
- <select
30
- {...props}
31
- className={cn(
32
- "block w-full min-w-0 disabled:cursor-not-allowed disabled:opacity-60 [color-scheme:var(--dashboard-color-scheme,dark)]",
33
- selectClassName[variant],
34
- showArrow
35
- ? "appearance-none pr-9 forced-colors:appearance-auto"
36
- : "pr-3",
37
- className,
38
- )}
39
- />
40
- {showArrow ? (
41
- <ChevronDown
42
- aria-hidden="true"
43
- className={cn(
44
- "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-dashboard-text-muted forced-colors:hidden",
45
- props.disabled && "opacity-60",
46
- )}
47
- size={16}
48
- />
49
- ) : null}
50
- </span>
51
- );
52
- }
@@ -1,17 +0,0 @@
1
- import type { ElementType } from "react";
2
-
3
- import { cn } from "../styles";
4
-
5
- /** Hide one content placeholder from assistive technology. */
6
- export function Skeleton(props: { as?: ElementType; className?: string }) {
7
- const Component = props.as ?? "div";
8
- return (
9
- <Component
10
- aria-hidden="true"
11
- className={cn(
12
- "animate-pulse rounded-md bg-dashboard-fill-soft motion-reduce:animate-none",
13
- props.className,
14
- )}
15
- />
16
- );
17
- }
@@ -1,58 +0,0 @@
1
- import type { InputHTMLAttributes, TextareaHTMLAttributes } from "react";
2
-
3
- import { cn } from "../styles";
4
-
5
- const textControlClassName =
6
- "block w-full rounded-md border border-dashboard-border-emphasis bg-dashboard-ink px-3 py-2 font-sans text-sm text-dashboard-text placeholder:text-dashboard-text-muted/70 focus:border-dashboard-focus focus:outline-none focus:ring-1 focus:ring-dashboard-focus/25 aria-invalid:border-rose-300 disabled:cursor-not-allowed disabled:opacity-50";
7
-
8
- export type TextInputProps = Omit<
9
- InputHTMLAttributes<HTMLInputElement>,
10
- "size"
11
- > & {
12
- className?: string;
13
- size?: "default" | "comfortable";
14
- };
15
-
16
- /** Render the dashboard's standard single-line text control. */
17
- export function TextInput({
18
- className,
19
- type = "text",
20
- size = "default",
21
- ...props
22
- }: TextInputProps) {
23
- return (
24
- <input
25
- {...props}
26
- className={cn(
27
- textControlClassName,
28
- size === "comfortable" && "min-h-11 text-base sm:min-h-10 sm:text-sm",
29
- className,
30
- )}
31
- type={type}
32
- />
33
- );
34
- }
35
-
36
- export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
37
- prose?: boolean;
38
- className?: string;
39
- };
40
-
41
- /** Render the dashboard's standard multi-line text control. */
42
- export function TextArea({
43
- className,
44
- prose = false,
45
- ...props
46
- }: TextAreaProps) {
47
- return (
48
- <textarea
49
- {...props}
50
- className={cn(
51
- textControlClassName,
52
- "min-h-36",
53
- prose ? "font-sans leading-relaxed" : "font-mono",
54
- className,
55
- )}
56
- />
57
- );
58
- }