@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.
- package/LICENSE +201 -0
- package/README.md +2 -5
- package/dist/app.js +119 -35
- package/dist/client.js +145 -136
- package/dist/index.d.ts +0 -1
- package/dist/index.js +119 -35
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +0 -1
- package/src/app.ts +5 -20
- package/src/client/App.tsx +29 -31
- package/src/client/api.ts +7 -0
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/BottomSheet.tsx +12 -2
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +82 -0
- package/src/client/components/DetailsPanel.tsx +133 -0
- package/src/client/components/Drawer.tsx +15 -24
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/FilterBar.tsx +16 -31
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Metric.tsx +33 -23
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/PageContentSkeleton.tsx +1 -1
- package/src/client/components/Pagination.tsx +27 -27
- package/src/client/components/ParticipantAvatarStack.tsx +43 -39
- package/src/client/components/ProfileMenu.tsx +27 -93
- package/src/client/components/README.md +103 -78
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/SegmentedNav.tsx +1 -1
- package/src/client/components/ShimmerText.tsx +2 -6
- package/src/client/components/ThemeSwitcher.tsx +19 -15
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/ActivityChart.tsx +173 -288
- package/src/client/components/charts/CacheWrites.tsx +59 -37
- package/src/client/components/charts/InputCacheChart.tsx +137 -131
- package/src/client/components/charts/README.md +4 -4
- package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
- package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
- package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
- package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
- package/src/client/components/layout/DashboardHeader.tsx +44 -90
- package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
- package/src/client/components/layout/VisualViewportShell.tsx +2 -2
- package/src/client/components/ui/accordion.tsx +77 -0
- package/src/client/components/ui/alert-dialog.tsx +89 -0
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button.tsx +86 -0
- package/src/client/components/ui/chart.tsx +74 -0
- package/src/client/components/ui/checkbox.tsx +26 -0
- package/src/client/components/ui/collapsible.tsx +22 -0
- package/src/client/components/ui/dialog.tsx +66 -0
- package/src/client/components/ui/empty.tsx +32 -0
- package/src/client/components/ui/field.tsx +49 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/input.tsx +35 -0
- package/src/client/components/ui/native-select.tsx +57 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/popover.tsx +67 -0
- package/src/client/components/ui/radio-group.tsx +32 -0
- package/src/client/components/ui/sheet.tsx +13 -3
- package/src/client/components/ui/skeleton.tsx +19 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +101 -0
- package/src/client/components/ui/tabs.tsx +75 -0
- package/src/client/components/ui/textarea.tsx +17 -0
- package/src/client/components/ui/toast.tsx +215 -0
- package/src/client/components/ui/toggle-group.tsx +36 -0
- package/src/client/components/ui/toggle.tsx +37 -0
- package/src/client/components/ui/tooltip.tsx +168 -0
- package/src/client/components/ui/trace.tsx +96 -0
- package/src/client/conversations/ChatLayout.tsx +7 -3
- package/src/client/conversations/ConversationBrief.tsx +17 -6
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationComposer.tsx +26 -12
- package/src/client/conversations/ConversationDetails.tsx +147 -62
- package/src/client/conversations/ConversationEventDetails.tsx +141 -0
- package/src/client/conversations/ConversationEventLog.tsx +150 -129
- package/src/client/conversations/ConversationHeader.tsx +13 -7
- package/src/client/conversations/ConversationHeaderActions.tsx +97 -93
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
- package/src/client/conversations/ConversationMeta.tsx +55 -88
- package/src/client/conversations/ConversationPage.tsx +220 -101
- package/src/client/conversations/ConversationSidebar.tsx +49 -29
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +173 -0
- package/src/client/conversations/ConversationWorkspace.tsx +12 -22
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
- package/src/client/conversations/DetailsList.tsx +139 -0
- package/src/client/conversations/EventDetails.tsx +42 -8
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +27 -16
- package/src/client/conversations/ObjectCard.tsx +20 -8
- package/src/client/conversations/PendingMailboxStack.tsx +52 -38
- package/src/client/conversations/README.md +89 -13
- package/src/client/conversations/ShareConversationButton.tsx +28 -17
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
- package/src/client/conversations/ToolFrame.tsx +36 -52
- package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
- package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
- package/src/client/conversations/TranscriptLoading.tsx +1 -1
- package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
- package/src/client/conversations/TranscriptMessageContextView.tsx +35 -18
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +34 -2
- package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
- package/src/client/conversations/TranscriptToolView.tsx +1 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +144 -65
- package/src/client/conversations/TranscriptView.tsx +63 -27
- package/src/client/conversations/eventLog.ts +58 -0
- package/src/client/conversations/eventTime.ts +60 -0
- package/src/client/conversations/queries.ts +29 -0
- package/src/client/conversations/transcriptBottomPinning.ts +34 -0
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/client/format.ts +5 -0
- package/src/client/pages/ConnectionsPage.tsx +2 -2
- package/src/client/pages/PersonalTokensPage.tsx +4 -4
- package/src/client/pages/SettingsPage.tsx +9 -10
- package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
- package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
- package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
- package/src/client/pages/automations/AutomationDetails.tsx +193 -0
- package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
- package/src/client/pages/automations/AutomationEditor.tsx +41 -29
- package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
- package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
- package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
- package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
- package/src/client/pages/automations/AutomationFilters.tsx +189 -74
- package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
- package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
- package/src/client/pages/automations/AutomationRow.tsx +12 -138
- package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
- package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
- package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
- package/src/client/pages/automations/AutomationStatus.tsx +201 -0
- package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
- package/src/client/pages/automations/AutomationsPage.tsx +80 -68
- package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
- package/src/client/pages/automations/README.md +57 -0
- package/src/client/pages/automations/TriggerFilters.tsx +22 -0
- package/src/client/pages/automations/automationFormat.ts +37 -23
- package/src/client/pages/code/CodeActivityChart.tsx +59 -101
- package/src/client/pages/code/CodePage.tsx +52 -55
- package/src/client/pages/code/RepositoryPage.tsx +6 -8
- package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
- package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
- package/src/client/pages/memory/MemoryDetails.tsx +218 -0
- package/src/client/pages/memory/MemoryPage.tsx +168 -112
- package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
- package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/people/ContributionGrid.tsx +23 -16
- package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
- package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
- package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
- package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
- package/src/client/pages/system/GuardianActivity.tsx +1 -2
- package/src/client/pages/system/PluginBarChart.tsx +106 -87
- package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
- package/src/client/pages/system/README.md +23 -0
- package/src/client/pages/system/SystemNavigation.tsx +5 -1
- package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
- package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
- package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
- package/src/client/pages/system/WorkspaceList.tsx +20 -30
- package/src/client/pages/system/WorkspacesPage.tsx +117 -119
- package/src/client/pages/system/sandboxSize.ts +8 -0
- package/src/client/pages/system/workspaceQueries.ts +53 -0
- package/src/client/pages/user/PluginUserPage.tsx +51 -2
- package/src/client/pages/user/pluginUserPageData.ts +1 -6
- package/src/client/styles.ts +4 -0
- package/src/client.tsx +7 -1
- package/src/mock-reporting/automation-collection.ts +4 -2
- package/src/mock-reporting/fixtures.ts +23 -7
- package/src/mock-reporting/routes.ts +12 -0
- package/src/mock-reporting/watches.ts +94 -0
- package/src/shell.ts +4 -9
- package/src/tailwind.css +0 -10
- package/src/client/components/Button.tsx +0 -118
- package/src/client/components/Field.tsx +0 -53
- package/src/client/components/SegmentedTabs.tsx +0 -119
- package/src/client/components/Select.tsx +0 -52
- package/src/client/components/Skeleton.tsx +0 -17
- package/src/client/components/TextInput.tsx +0 -58
- package/src/client/components/Tooltip.tsx +0 -216
- package/src/client/components/charts/useChartLayout.ts +0 -27
- package/src/client/components/ui/sheet.LICENSE +0 -21
- package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
- package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
- package/src/client/pages/dev/ChartFixtures.tsx +0 -325
- package/src/client/pages/dev/ComponentsPage.tsx +0 -899
- package/src/client/pages/dev/ConversationFixture.tsx +0 -231
- package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
- package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
- package/src/client/pages/dev/SelectFixtures.tsx +0 -62
- package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
- package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
- 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
|
+
// the timer fire time stays in the future.
|
|
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
|
-
|
|
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,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
|
-
}
|