@sentry/junior-dashboard 0.263.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 (197) 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 +148 -135
  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 +9 -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 +26 -18
  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 +34 -33
  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 +101 -64
  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 +151 -0
  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/components/ui/useSheet.ts +74 -0
  60. package/src/client/conversations/ChatLayout.tsx +7 -3
  61. package/src/client/conversations/ConversationBrief.tsx +17 -6
  62. package/src/client/conversations/ConversationChildAgents.tsx +87 -58
  63. package/src/client/conversations/ConversationComposer.tsx +26 -12
  64. package/src/client/conversations/ConversationDetails.tsx +198 -95
  65. package/src/client/conversations/ConversationEventDetails.tsx +141 -0
  66. package/src/client/conversations/ConversationEventLog.tsx +150 -129
  67. package/src/client/conversations/ConversationHeader.tsx +8 -3
  68. package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
  69. package/src/client/conversations/ConversationHomeList.tsx +1 -1
  70. package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
  71. package/src/client/conversations/ConversationMeta.tsx +111 -92
  72. package/src/client/conversations/ConversationPage.tsx +241 -110
  73. package/src/client/conversations/ConversationSidebar.tsx +48 -29
  74. package/src/client/conversations/ConversationUsageBreakdown.tsx +16 -3
  75. package/src/client/conversations/ConversationWatches.tsx +165 -0
  76. package/src/client/conversations/ConversationWorkspace.tsx +12 -22
  77. package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
  78. package/src/client/conversations/DetailsList.tsx +124 -0
  79. package/src/client/conversations/EventDetails.tsx +42 -8
  80. package/src/client/conversations/ForkConversationButton.tsx +27 -16
  81. package/src/client/conversations/ObjectCard.tsx +20 -8
  82. package/src/client/conversations/PendingMailboxStack.tsx +52 -33
  83. package/src/client/conversations/README.md +93 -19
  84. package/src/client/conversations/ShareConversationButton.tsx +28 -3
  85. package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
  86. package/src/client/conversations/ToolFrame.tsx +36 -52
  87. package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
  88. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
  89. package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
  90. package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
  91. package/src/client/conversations/TranscriptLoading.tsx +1 -1
  92. package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
  93. package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
  94. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  95. package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
  96. package/src/client/conversations/TranscriptSubagentView.tsx +3 -3
  97. package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
  98. package/src/client/conversations/TranscriptToolView.tsx +1 -1
  99. package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
  100. package/src/client/conversations/TranscriptView.tsx +63 -27
  101. package/src/client/conversations/eventLog.ts +57 -0
  102. package/src/client/conversations/eventTime.ts +60 -0
  103. package/src/client/conversations/queries.ts +29 -0
  104. package/src/client/conversations/transcriptBottomPinning.ts +34 -0
  105. package/src/client/format.ts +5 -0
  106. package/src/client/lib/utils.ts +1 -0
  107. package/src/client/pages/ConnectionsPage.tsx +2 -2
  108. package/src/client/pages/PersonalTokensPage.tsx +4 -4
  109. package/src/client/pages/SettingsPage.tsx +9 -10
  110. package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
  111. package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
  112. package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
  113. package/src/client/pages/automations/AutomationDetails.tsx +193 -0
  114. package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
  115. package/src/client/pages/automations/AutomationEditor.tsx +41 -29
  116. package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
  117. package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
  118. package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
  119. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
  120. package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
  121. package/src/client/pages/automations/AutomationFilters.tsx +189 -74
  122. package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
  123. package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
  124. package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
  125. package/src/client/pages/automations/AutomationRow.tsx +12 -138
  126. package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
  127. package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
  128. package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
  129. package/src/client/pages/automations/AutomationStatus.tsx +201 -0
  130. package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
  131. package/src/client/pages/automations/AutomationsPage.tsx +80 -68
  132. package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
  133. package/src/client/pages/automations/README.md +57 -0
  134. package/src/client/pages/automations/TriggerFilters.tsx +22 -0
  135. package/src/client/pages/automations/automationFormat.ts +37 -23
  136. package/src/client/pages/code/CodeActivityChart.tsx +59 -101
  137. package/src/client/pages/code/CodePage.tsx +52 -55
  138. package/src/client/pages/code/RepositoryPage.tsx +6 -8
  139. package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
  140. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
  141. package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
  142. package/src/client/pages/memory/MemoryDetails.tsx +218 -0
  143. package/src/client/pages/memory/MemoryPage.tsx +168 -112
  144. package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
  145. package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
  146. package/src/client/pages/memory/memoryRecord.ts +1 -1
  147. package/src/client/pages/people/ContributionGrid.tsx +23 -16
  148. package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
  149. package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
  150. package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
  151. package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
  152. package/src/client/pages/system/GuardianActivity.tsx +1 -2
  153. package/src/client/pages/system/PluginBarChart.tsx +106 -87
  154. package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
  155. package/src/client/pages/system/README.md +23 -0
  156. package/src/client/pages/system/SystemNavigation.tsx +5 -1
  157. package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
  158. package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
  159. package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
  160. package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
  161. package/src/client/pages/system/WorkspaceList.tsx +20 -30
  162. package/src/client/pages/system/WorkspacesPage.tsx +117 -119
  163. package/src/client/pages/system/sandboxSize.ts +8 -0
  164. package/src/client/pages/system/workspaceQueries.ts +53 -0
  165. package/src/client/pages/user/PluginUserPage.tsx +51 -2
  166. package/src/client/pages/user/pluginUserPageData.ts +1 -6
  167. package/src/client/styles.ts +4 -0
  168. package/src/client.tsx +7 -1
  169. package/src/mock-reporting/automation-collection.ts +4 -2
  170. package/src/mock-reporting/fixtures.ts +23 -7
  171. package/src/mock-reporting/routes.ts +12 -0
  172. package/src/mock-reporting/watches.ts +94 -0
  173. package/src/shell.ts +4 -9
  174. package/src/tailwind.css +77 -10
  175. package/src/client/components/Button.tsx +0 -118
  176. package/src/client/components/Field.tsx +0 -53
  177. package/src/client/components/SegmentedTabs.tsx +0 -119
  178. package/src/client/components/Select.tsx +0 -52
  179. package/src/client/components/Sheet.LICENSE +0 -21
  180. package/src/client/components/Sheet.tsx +0 -141
  181. package/src/client/components/Skeleton.tsx +0 -17
  182. package/src/client/components/TextInput.tsx +0 -58
  183. package/src/client/components/Tooltip.tsx +0 -216
  184. package/src/client/components/charts/useChartLayout.ts +0 -27
  185. package/src/client/conversations/ConversationDetailsFrame.tsx +0 -91
  186. package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
  187. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
  188. package/src/client/pages/dev/ChartFixtures.tsx +0 -325
  189. package/src/client/pages/dev/ComponentsPage.tsx +0 -899
  190. package/src/client/pages/dev/ConversationFixture.tsx +0 -231
  191. package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
  192. package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
  193. package/src/client/pages/dev/SelectFixtures.tsx +0 -62
  194. package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
  195. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
  196. package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
  197. 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
@@ -1,5 +1,7 @@
1
1
  @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@300;400;500;600;700&display=swap");
2
2
  @import "tailwindcss";
3
+ @import "tw-animate-css";
4
+ @import "shadcn/tailwind.css";
3
5
 
4
6
  /*
5
7
  * Dashboard design tokens. components/README.md explains each role.
@@ -214,6 +216,81 @@
214
216
  --color-violet-400: oklch(54.1% 0.281 293.009);
215
217
  }
216
218
 
219
+ /* shadcn components use the dashboard tokens in both themes. */
220
+ @layer base {
221
+ :root {
222
+ --radius: 0.5rem;
223
+ --background: var(--color-dashboard-bg);
224
+ --foreground: var(--color-dashboard-text);
225
+ --card: var(--color-dashboard-surface-panel);
226
+ --card-foreground: var(--color-dashboard-text);
227
+ --popover: var(--color-dashboard-bg-elevated);
228
+ --popover-foreground: var(--color-dashboard-text);
229
+ --primary: var(--color-dashboard-text-solid);
230
+ --primary-foreground: var(--color-dashboard-text-inverse);
231
+ --secondary: var(--color-dashboard-surface-raised);
232
+ --secondary-foreground: var(--color-dashboard-text);
233
+ --muted: var(--color-dashboard-surface-raised);
234
+ --muted-foreground: var(--color-dashboard-text-muted);
235
+ --accent: var(--color-dashboard-surface-hover);
236
+ --accent-foreground: var(--color-dashboard-text);
237
+ --destructive: var(--color-rose-300);
238
+ --border: var(--color-dashboard-border);
239
+ --input: var(--color-dashboard-border-emphasis);
240
+ --ring: var(--color-dashboard-focus);
241
+ --chart-1: var(--color-violet-400);
242
+ --chart-2: var(--color-sky-400);
243
+ --chart-3: var(--color-emerald-400);
244
+ --chart-4: var(--color-amber-400);
245
+ --chart-5: var(--color-rose-400);
246
+ --sidebar: var(--color-dashboard-surface-panel);
247
+ --sidebar-foreground: var(--color-dashboard-text);
248
+ --sidebar-primary: var(--primary);
249
+ --sidebar-primary-foreground: var(--color-dashboard-text-inverse);
250
+ --sidebar-accent: var(--color-dashboard-surface-hover);
251
+ --sidebar-accent-foreground: var(--color-dashboard-text);
252
+ --sidebar-border: var(--color-dashboard-border);
253
+ --sidebar-ring: var(--color-dashboard-focus);
254
+ }
255
+ }
256
+
257
+ @theme inline {
258
+ --color-background: var(--background);
259
+ --color-foreground: var(--foreground);
260
+ --color-card: var(--card);
261
+ --color-card-foreground: var(--card-foreground);
262
+ --color-popover: var(--popover);
263
+ --color-popover-foreground: var(--popover-foreground);
264
+ --color-primary: var(--primary);
265
+ --color-primary-foreground: var(--primary-foreground);
266
+ --color-secondary: var(--secondary);
267
+ --color-secondary-foreground: var(--secondary-foreground);
268
+ --color-muted: var(--muted);
269
+ --color-muted-foreground: var(--muted-foreground);
270
+ --color-accent: var(--accent);
271
+ --color-accent-foreground: var(--accent-foreground);
272
+ --color-destructive: var(--destructive);
273
+ --color-border: var(--border);
274
+ --color-input: var(--input);
275
+ --color-ring: var(--ring);
276
+ --color-chart-1: var(--chart-1);
277
+ --color-chart-2: var(--chart-2);
278
+ --color-chart-3: var(--chart-3);
279
+ --color-chart-4: var(--chart-4);
280
+ --color-chart-5: var(--chart-5);
281
+ --color-sidebar: var(--sidebar);
282
+ --color-sidebar-foreground: var(--sidebar-foreground);
283
+ --color-sidebar-primary: var(--sidebar-primary);
284
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
285
+ --color-sidebar-accent: var(--sidebar-accent);
286
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
287
+ --color-sidebar-border: var(--sidebar-border);
288
+ --color-sidebar-ring: var(--sidebar-ring);
289
+ }
290
+
291
+ /* The dashboard defaults to dark and selects light with data-theme. */
292
+ @custom-variant dark (&:where(:root:not([data-theme="light"]), :root:not([data-theme="light"]) *));
293
+
217
294
  /* Style one-off values that the token and palette mirror cannot cover. */
218
295
  @custom-variant light (&:where([data-theme="light"], [data-theme="light"] *));
219
296
 
@@ -261,16 +338,6 @@
261
338
  }
262
339
  }
263
340
 
264
- @keyframes junior-text-shimmer {
265
- 0% {
266
- background-position: 200% 50%;
267
- }
268
-
269
- 100% {
270
- background-position: -200% 50%;
271
- }
272
- }
273
-
274
341
  @keyframes junior-active-indicator {
275
342
  0%,
276
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,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 shadcn
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.