@sentry/junior-dashboard 0.258.0 → 0.260.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 (65) hide show
  1. package/README.md +5 -3
  2. package/dist/app.js +88 -37
  3. package/dist/chunks/github-light-QHW3AEMR.js +1 -0
  4. package/dist/client.js +86 -50
  5. package/dist/index.js +88 -37
  6. package/dist/tailwind.css +1 -1
  7. package/package.json +3 -3
  8. package/src/client/App.tsx +0 -1
  9. package/src/client/code.tsx +10 -4
  10. package/src/client/components/ProfileMenu.tsx +3 -0
  11. package/src/client/components/README.md +12 -2
  12. package/src/client/components/ThemeSwitcher.tsx +48 -0
  13. package/src/client/components/charts/ActivityChart.tsx +6 -7
  14. package/src/client/components/charts/InputCacheChart.tsx +1 -1
  15. package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
  16. package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
  17. package/src/client/components/charts/cache-input.ts +11 -3
  18. package/src/client/components/layout/DashboardHeader.tsx +2 -9
  19. package/src/client/conversations/ConversationBrief.tsx +35 -9
  20. package/src/client/conversations/ConversationHomeList.tsx +7 -1
  21. package/src/client/conversations/ConversationMeta.tsx +19 -7
  22. package/src/client/conversations/ConversationTranscript.tsx +8 -11
  23. package/src/client/conversations/EventDetails.tsx +9 -5
  24. package/src/client/conversations/README.md +6 -0
  25. package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
  26. package/src/client/conversations/TranscriptEventView.tsx +48 -9
  27. package/src/client/conversations/TranscriptMessageView.tsx +6 -1
  28. package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
  29. package/src/client/conversations/TranscriptText.tsx +28 -20
  30. package/src/client/conversations/conversationActivityPreview.ts +6 -0
  31. package/src/client/conversations/eventTranscript.ts +4 -1
  32. package/src/client/conversations/transcriptMentions.tsx +27 -10
  33. package/src/client/pages/admin/AdminPage.tsx +14 -5
  34. package/src/client/pages/admin/ServiceAccountsSection.tsx +231 -0
  35. package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
  36. package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
  37. package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
  38. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
  39. package/src/client/pages/code/CodeActivityChart.tsx +4 -4
  40. package/src/client/pages/dev/ComponentsPage.tsx +7 -3
  41. package/src/client/pages/dev/ConversationFixture.tsx +29 -0
  42. package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
  43. package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
  44. package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
  45. package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
  46. package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
  47. package/src/client/pages/people/ContributionGrid.tsx +8 -8
  48. package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
  49. package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
  50. package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
  51. package/src/client/pages/system/PluginBarChart.tsx +12 -6
  52. package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
  53. package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
  54. package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
  55. package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
  56. package/src/client/pages/system/WorkspaceList.tsx +6 -1
  57. package/src/client/pages/system/WorkspacesPage.tsx +18 -12
  58. package/src/client/pages/system/workspaceDraft.ts +8 -0
  59. package/src/client/theme.ts +52 -0
  60. package/src/client/types.ts +8 -1
  61. package/src/mock-reporting/fixtures.ts +48 -35
  62. package/src/shell.ts +4 -1
  63. package/src/tailwind.css +109 -0
  64. package/src/theme.ts +49 -0
  65. package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
@@ -2,7 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { Plus } from "lucide-react";
3
3
  import {
4
4
  workspaceListSchema,
5
- type BaselineSnapshotReport,
5
+ type WorkspaceListReport,
6
6
  type WorkspaceReport,
7
7
  } from "@sentry/junior/api/schema";
8
8
 
@@ -17,7 +17,7 @@ import {
17
17
  deleteDashboardResource,
18
18
  fetchDashboardJson,
19
19
  } from "../../http";
20
- import { BaselineSnapshotCard } from "./BaselineSnapshotCard";
20
+ import { BaseSandboxCard } from "./BaseSandboxCard";
21
21
  import { SystemPageLayout, SystemRouteLoading } from "./SystemPageLayout";
22
22
  import { WorkspaceList } from "./WorkspaceList";
23
23
 
@@ -55,15 +55,16 @@ export function WorkspacesPage() {
55
55
  },
56
56
  onSuccess: async (workspace) => {
57
57
  await queryClient.cancelQueries({ queryKey: workspacesQueryKey });
58
- queryClient.setQueryData<{
59
- baselineSnapshot: BaselineSnapshotReport | null;
60
- workspaces: WorkspaceReport[];
61
- }>(workspacesQueryKey, (current) => ({
62
- baselineSnapshot: current?.baselineSnapshot ?? null,
63
- workspaces: (current?.workspaces ?? []).filter(
64
- (item) => item.id !== workspace.id,
65
- ),
66
- }));
58
+ queryClient.setQueryData<WorkspaceListReport>(
59
+ workspacesQueryKey,
60
+ (current) =>
61
+ current && {
62
+ ...current,
63
+ workspaces: current.workspaces.filter(
64
+ (item) => item.id !== workspace.id,
65
+ ),
66
+ },
67
+ );
67
68
  },
68
69
  });
69
70
 
@@ -80,6 +81,7 @@ export function WorkspacesPage() {
80
81
 
81
82
  const workspaces = workspacesQuery.data?.workspaces ?? [];
82
83
  const baselineSnapshot = workspacesQuery.data?.baselineSnapshot ?? null;
84
+ const defaultVcpus = workspacesQuery.data?.defaultVcpus ?? null;
83
85
  return (
84
86
  <SystemPageLayout>
85
87
  <PageHeader
@@ -111,11 +113,15 @@ export function WorkspacesPage() {
111
113
  ) : null}
112
114
 
113
115
  {!workspacesQuery.error ? (
114
- <BaselineSnapshotCard snapshot={baselineSnapshot} />
116
+ <BaseSandboxCard
117
+ defaultVcpus={defaultVcpus}
118
+ snapshot={baselineSnapshot}
119
+ />
115
120
  ) : null}
116
121
 
117
122
  <WorkspaceList
118
123
  busy={deleteMutation.isPending}
124
+ defaultVcpus={defaultVcpus}
119
125
  onDelete={(workspace) => {
120
126
  if (
121
127
  window.confirm(
@@ -9,6 +9,8 @@ export type RepoDraft = {
9
9
  export type WorkspaceDraft = {
10
10
  name: string;
11
11
  setupScript: string;
12
+ /** Text input value. Empty means the app default. */
13
+ vcpus: string;
12
14
  repos: RepoDraft[];
13
15
  };
14
16
 
@@ -24,6 +26,7 @@ export function createWorkspaceDraft(): WorkspaceDraft {
24
26
  return {
25
27
  name: "",
26
28
  setupScript: "",
29
+ vcpus: "",
27
30
  repos: [createRepoDraft()],
28
31
  };
29
32
  }
@@ -32,6 +35,7 @@ export function editWorkspaceDraft(workspace: WorkspaceReport): WorkspaceDraft {
32
35
  return {
33
36
  name: workspace.name,
34
37
  setupScript: workspace.setupScript,
38
+ vcpus: workspace.vcpus === null ? "" : String(workspace.vcpus),
35
39
  repos:
36
40
  workspace.repos.length > 0
37
41
  ? workspace.repos.map((repo) => ({
@@ -48,6 +52,7 @@ export function workspaceDraftBody(draft: WorkspaceDraft) {
48
52
  return {
49
53
  name: draft.name.trim(),
50
54
  setupScript: draft.setupScript,
55
+ vcpus: draft.vcpus.trim() ? Number(draft.vcpus.trim()) : null,
51
56
  repos: draft.repos.map((repo) => ({
52
57
  provider: repo.provider.trim(),
53
58
  repo: repo.repo.trim(),
@@ -60,6 +65,9 @@ export function canSaveWorkspaceDraft(
60
65
  busy: boolean,
61
66
  ): boolean {
62
67
  if (busy || !draft.name.trim()) return false;
68
+ if (draft.vcpus.trim() && !/^[1-9]\d*$/.test(draft.vcpus.trim())) {
69
+ return false;
70
+ }
63
71
  if (draft.repos.some((repo) => !repo.provider.trim() || !repo.repo.trim())) {
64
72
  return false;
65
73
  }
@@ -0,0 +1,52 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ import {
4
+ DASHBOARD_THEME_CHANGE_EVENT,
5
+ DASHBOARD_THEME_STORAGE_KEY,
6
+ type DashboardThemeChoice,
7
+ } from "../theme";
8
+
9
+ function readThemeChoice(): DashboardThemeChoice {
10
+ try {
11
+ const choice = window.localStorage.getItem(DASHBOARD_THEME_STORAGE_KEY);
12
+ return choice === "light" || choice === "dark" ? choice : "system";
13
+ } catch {
14
+ return "system";
15
+ }
16
+ }
17
+
18
+ function subscribe(onChange: () => void): () => void {
19
+ window.addEventListener(DASHBOARD_THEME_CHANGE_EVENT, onChange);
20
+ window.addEventListener("storage", onChange);
21
+ return () => {
22
+ window.removeEventListener(DASHBOARD_THEME_CHANGE_EVENT, onChange);
23
+ window.removeEventListener("storage", onChange);
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Save a theme choice for this browser. The shell theme script applies it
29
+ * and keeps following the system color scheme for `system`.
30
+ */
31
+ function saveThemeChoice(choice: DashboardThemeChoice): void {
32
+ try {
33
+ if (choice === "system") {
34
+ window.localStorage.removeItem(DASHBOARD_THEME_STORAGE_KEY);
35
+ } else {
36
+ window.localStorage.setItem(DASHBOARD_THEME_STORAGE_KEY, choice);
37
+ }
38
+ } catch {
39
+ // Storage is blocked, so the dashboard keeps following the system.
40
+ return;
41
+ }
42
+ window.dispatchEvent(new Event(DASHBOARD_THEME_CHANGE_EVENT));
43
+ }
44
+
45
+ /** Read and update this browser's dashboard theme choice. */
46
+ export function useDashboardThemeChoice(): [
47
+ DashboardThemeChoice,
48
+ (choice: DashboardThemeChoice) => void,
49
+ ] {
50
+ const choice = useSyncExternalStore(subscribe, readThemeChoice);
51
+ return [choice, saveThemeChoice];
52
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  } from "@sentry/junior/api/schema";
10
10
  import type {
11
11
  ActorIdentity,
12
+ ConversationReportMessageEvent,
12
13
  ConversationStatsReport,
13
14
  ConversationSummaryReport,
14
15
  ConversationTurnFailureCode,
@@ -51,6 +52,8 @@ export type TranscriptViewSubagentPart = {
51
52
  };
52
53
 
53
54
  export type TranscriptViewContextEventPart = {
55
+ /** People mentioned in the summary, resolved by the API. */
56
+ mentions?: ActorIdentity[];
54
57
  event:
55
58
  | {
56
59
  createdAt: string;
@@ -81,6 +84,8 @@ export type TranscriptViewContextEventPart = {
81
84
  };
82
85
 
83
86
  export type TranscriptViewStructuredEventPart = {
87
+ /** People mentioned in the preview and details, resolved by the API. */
88
+ mentions?: ActorIdentity[];
84
89
  name: string;
85
90
  namespace: string;
86
91
  presentation: ConversationEventPresentation;
@@ -120,9 +125,11 @@ export type TranscriptViewMessage = {
120
125
  eventObjectType?: ObjectType;
121
126
  /** Short summary supplied by the Event publisher. */
122
127
  trustedSummary?: string;
128
+ /** Every Event when several arrived together in one message. */
129
+ events?: ConversationReportMessageEvent[];
123
130
  /** Whether the source message addressed Junior directly. */
124
131
  explicitMention?: boolean;
125
- /** People mentioned in the message text. */
132
+ /** People mentioned in the event text, resolved by the API. */
126
133
  mentions?: ActorIdentity[];
127
134
  /** Whether a non-mention message was used as input to a turn. */
128
135
  context?: boolean;
@@ -168,7 +168,7 @@ function reportEvent(
168
168
  seq: number,
169
169
  createdAt: string,
170
170
  data: ConversationReportEventData,
171
- metadata?: Pick<ConversationReportEvent, "model" | "modelCall">,
171
+ metadata?: Pick<ConversationReportEvent, "mentions" | "model" | "modelCall">,
172
172
  ): ConversationReportEvent {
173
173
  return { seq, createdAt, data, ...metadata };
174
174
  }
@@ -1428,48 +1428,58 @@ function eventAutomationRunConversation(
1428
1428
  credentialUser: "Alex Rivera",
1429
1429
  },
1430
1430
  events: [
1431
- reportEvent(0, startedAt, {
1432
- type: "message",
1433
- messageId: EVENT_AUTOMATION_RUN_CONVERSATION_ID,
1434
- role: "user",
1435
- // Dispatch input is authored by the dispatch owner, not a person.
1436
- actorIdentity: {
1437
- fullName: "junior",
1438
- slackUserId: "junior",
1439
- slackUserName: "junior",
1431
+ reportEvent(
1432
+ 0,
1433
+ startedAt,
1434
+ {
1435
+ type: "message",
1436
+ messageId: EVENT_AUTOMATION_RUN_CONVERSATION_ID,
1437
+ role: "user",
1438
+ // Dispatch input is authored by the dispatch owner, not a person.
1439
+ actorIdentity: {
1440
+ fullName: "junior",
1441
+ slackUserId: "junior",
1442
+ slackUserName: "junior",
1443
+ },
1444
+ text: [
1445
+ "[task]",
1446
+ "",
1447
+ "This is a task, not a message from a person.",
1448
+ "",
1449
+ "About: Issue · ACME-42",
1450
+ 'Created by: <@U123>. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person.',
1451
+ "Instructions: Summarize the closed issue",
1452
+ "",
1453
+ "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.",
1454
+ "",
1455
+ "End with `finishAutomationRun`.",
1456
+ ].join("\n"),
1440
1457
  },
1441
- text: [
1442
- "[task]",
1443
- "",
1444
- "This is a task, not a message from a person.",
1445
- "",
1446
- "About: Issue · ACME-42",
1447
- 'Created by: <@U123>. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person.',
1448
- "Instructions: Summarize the closed issue",
1449
- "",
1450
- "Trusted summary: ACME-42 Checkout fails for saved cards was closed by Morgan Lee.",
1451
- "",
1452
- "End with `finishAutomationRun`.",
1453
- ].join("\n"),
1454
- mentions: [MOCK_MENTION_MORGAN],
1455
- }),
1458
+ { mentions: [MOCK_MENTION_MORGAN] },
1459
+ ),
1456
1460
  reportEvent(1, at(1_000), {
1457
1461
  type: "turn_lifecycle",
1458
1462
  turnId: "dispatch:event-1",
1459
1463
  state: "started",
1460
1464
  inputMessageIds: [EVENT_AUTOMATION_RUN_CONVERSATION_ID],
1461
1465
  }),
1462
- reportEvent(2, at(13_000), {
1463
- type: "message",
1464
- messageId: `${EVENT_AUTOMATION_RUN_CONVERSATION_ID}:reply`,
1465
- role: "assistant",
1466
- text: "<@U123> ACME-42 is closed. The saved-card checkout failure was fixed by the token refresh change and verified in production. cc <@U456> and <@U999> in <#C42|proj-checkout>.",
1466
+ reportEvent(
1467
+ 2,
1468
+ at(13_000),
1469
+ {
1470
+ type: "message",
1471
+ messageId: `${EVENT_AUTOMATION_RUN_CONVERSATION_ID}:reply`,
1472
+ role: "assistant",
1473
+ text: "<@U123> ACME-42 is closed. The saved-card checkout failure was fixed by the token refresh change and verified in production. cc <@U456> and <@U999> in <#C42|proj-checkout>.",
1474
+ },
1467
1475
  // U456 has no People profile, and U999 is not resolved.
1468
- mentions: [
1469
- MOCK_MENTION_MORGAN,
1470
- { fullName: "Casey Nguyen", slackUserId: "U456" },
1471
- ],
1472
- }),
1476
+ {
1477
+ mentions: [
1478
+ MOCK_MENTION_MORGAN,
1479
+ { fullName: "Casey Nguyen", slackUserId: "U456" },
1480
+ ],
1481
+ },
1482
+ ),
1473
1483
  reportEvent(3, at(14_000), {
1474
1484
  type: "turn_lifecycle",
1475
1485
  turnId: "dispatch:event-1",
@@ -1601,6 +1611,9 @@ function summaryFromConversation(
1601
1611
  activityEvent?.data.type === "message" && activityEvent.data.text
1602
1612
  ? {
1603
1613
  createdAt: activityEvent.createdAt,
1614
+ ...(activityEvent.mentions
1615
+ ? { mentions: activityEvent.mentions }
1616
+ : undefined),
1604
1617
  role: activityEvent.data.role as "assistant" | "user",
1605
1618
  text: activityEvent.data.text,
1606
1619
  }
package/src/shell.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  dashboardTailwindAsset,
10
10
  } from "./assets";
11
11
  import { dashboardRainbowProgressClass } from "./dashboardLoader";
12
+ import { DASHBOARD_THEME_COLORS, DASHBOARD_THEME_SCRIPT } from "./theme";
12
13
 
13
14
  const DASHBOARD_CLIENT_VERSION = Date.now().toString(36);
14
15
  export const DASHBOARD_CLIENT_PATH = "/_junior/dashboard/client.js";
@@ -16,7 +17,7 @@ export const DASHBOARD_AVATAR_HEADER_PATH = "/_junior/dashboard/avatar.png";
16
17
  export const DASHBOARD_INSTALL_ICON_PATH = "/_junior/dashboard/icon-512.png";
17
18
  export const DASHBOARD_MANIFEST_PATH =
18
19
  "/_junior/dashboard/manifest.webmanifest";
19
- const DASHBOARD_THEME_COLOR = "#000000";
20
+ const DASHBOARD_THEME_COLOR = DASHBOARD_THEME_COLORS.dark;
20
21
  const DASHBOARD_BACKGROUND_COLOR = "#000000";
21
22
 
22
23
  function escapeHtml(value: string): string {
@@ -224,6 +225,7 @@ export function renderDashboard(basePath: string, agentName: string): Response {
224
225
  <meta charset="utf-8" />
225
226
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
226
227
  <meta name="theme-color" content="${DASHBOARD_THEME_COLOR}" />
228
+ <script>${DASHBOARD_THEME_SCRIPT}</script>
227
229
  <meta name="mobile-web-app-capable" content="yes" />
228
230
  <meta name="apple-mobile-web-app-capable" content="yes" />
229
231
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
@@ -334,6 +336,7 @@ export function renderForbiddenPage(agentName: string): Response {
334
336
  <meta charset="utf-8" />
335
337
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
336
338
  <title>${escapeHtml(agentName)} access denied</title>
339
+ <script>${DASHBOARD_THEME_SCRIPT}</script>
337
340
  <style>
338
341
  ${readDashboardTailwind()}
339
342
  </style>
package/src/tailwind.css CHANGED
@@ -108,6 +108,115 @@
108
108
  --color-dashboard-scrollbar-thumb-hover: rgb(255 255 255 / 0.2);
109
109
  }
110
110
 
111
+ /*
112
+ * Light theme. src/theme.ts sets data-theme on <html> before first paint from
113
+ * the saved choice or the system color scheme. Each dashboard token above gets
114
+ * a light value here.
115
+ */
116
+ :root[data-theme="light"] {
117
+ --dashboard-color-scheme: light;
118
+
119
+ --color-dashboard-bg: #ffffff;
120
+ --color-dashboard-bg-elevated: #ffffff;
121
+ --color-dashboard-surface-panel: #f9f9fa;
122
+ --color-dashboard-surface-active: #e9e9ec;
123
+ --color-dashboard-surface-raised: #ffffff;
124
+ --color-dashboard-surface-hover: #f1f1f3;
125
+ --color-dashboard-control: #ffffff;
126
+ --color-dashboard-ink: #ffffff;
127
+
128
+ --color-dashboard-text: #18181b;
129
+ --color-dashboard-text-muted: #52525b;
130
+ --color-dashboard-text-subtle: #3f3f46;
131
+ --color-dashboard-text-faint: #71717a;
132
+ --color-dashboard-text-solid: #09090b;
133
+ --color-dashboard-text-inverse: #ffffff;
134
+
135
+ --color-dashboard-accent: #7553ff;
136
+ --color-dashboard-accent-text: #5b3bd9;
137
+ --color-dashboard-focus: #7553ff;
138
+
139
+ --color-dashboard-border-subtle: rgb(0 0 0 / 0.06);
140
+ --color-dashboard-border: rgb(0 0 0 / 0.09);
141
+ --color-dashboard-border-strong: rgb(0 0 0 / 0.11);
142
+ --color-dashboard-border-emphasis: rgb(0 0 0 / 0.16);
143
+ --color-dashboard-border-interactive: rgb(0 0 0 / 0.26);
144
+ --color-dashboard-border-heavy: rgb(0 0 0 / 0.32);
145
+
146
+ --color-dashboard-fill-faint: rgb(0 0 0 / 0.02);
147
+ --color-dashboard-fill-soft: rgb(0 0 0 / 0.035);
148
+ --color-dashboard-fill-hover: rgb(0 0 0 / 0.05);
149
+ --color-dashboard-fill-strong: rgb(0 0 0 / 0.08);
150
+
151
+ --color-dashboard-overlay-soft: rgb(0 0 0 / 0.06);
152
+ --color-dashboard-overlay: rgb(0 0 0 / 0.35);
153
+
154
+ --color-dashboard-scrollbar-track: rgb(0 0 0 / 0.02);
155
+ --color-dashboard-scrollbar-thumb: rgb(0 0 0 / 0.14);
156
+ --color-dashboard-scrollbar-thumb-hover: rgb(0 0 0 / 0.26);
157
+
158
+ /*
159
+ * Status and chart hues are tuned for the dark canvas: the UI uses their
160
+ * light shades (50-400). Mirror those shades (50 to 950, 100 to 900, and so
161
+ * on) so the same classes keep their contrast on the light canvas. Shades
162
+ * 500 and darker keep their values. Add a hue here when the UI starts to
163
+ * use it.
164
+ */
165
+ --color-amber-50: oklch(27.9% 0.077 45.635);
166
+ --color-amber-100: oklch(41.4% 0.112 45.904);
167
+ --color-amber-200: oklch(47.3% 0.137 46.201);
168
+ --color-amber-300: oklch(55.5% 0.163 48.998);
169
+ --color-amber-400: oklch(66.6% 0.179 58.318);
170
+ --color-cyan-50: oklch(30.2% 0.056 229.695);
171
+ --color-cyan-100: oklch(39.8% 0.07 227.392);
172
+ --color-cyan-200: oklch(45% 0.085 224.283);
173
+ --color-cyan-300: oklch(52% 0.105 223.128);
174
+ --color-cyan-400: oklch(60.9% 0.126 221.723);
175
+ --color-emerald-50: oklch(26.2% 0.051 172.552);
176
+ --color-emerald-100: oklch(37.8% 0.077 168.94);
177
+ --color-emerald-200: oklch(43.2% 0.095 166.913);
178
+ --color-emerald-300: oklch(50.8% 0.118 165.612);
179
+ --color-emerald-400: oklch(59.6% 0.145 163.225);
180
+ --color-fuchsia-50: oklch(29.3% 0.136 325.661);
181
+ --color-fuchsia-100: oklch(40.1% 0.17 325.612);
182
+ --color-fuchsia-200: oklch(45.2% 0.211 324.591);
183
+ --color-fuchsia-300: oklch(51.8% 0.253 323.949);
184
+ --color-fuchsia-400: oklch(59.1% 0.293 322.896);
185
+ --color-orange-50: oklch(26.6% 0.079 36.259);
186
+ --color-orange-100: oklch(40.8% 0.123 38.172);
187
+ --color-orange-200: oklch(47% 0.157 37.304);
188
+ --color-orange-300: oklch(55.3% 0.195 38.402);
189
+ --color-orange-400: oklch(64.6% 0.222 41.116);
190
+ --color-red-50: oklch(25.8% 0.092 26.042);
191
+ --color-red-100: oklch(39.6% 0.141 25.723);
192
+ --color-red-200: oklch(44.4% 0.177 26.899);
193
+ --color-red-300: oklch(50.5% 0.213 27.518);
194
+ --color-red-400: oklch(57.7% 0.245 27.325);
195
+ --color-rose-50: oklch(27.1% 0.105 12.094);
196
+ --color-rose-100: oklch(41% 0.159 10.272);
197
+ --color-rose-200: oklch(45.5% 0.188 13.697);
198
+ --color-rose-300: oklch(51.4% 0.222 16.935);
199
+ --color-rose-400: oklch(58.6% 0.253 17.585);
200
+ --color-sky-50: oklch(29.3% 0.066 243.157);
201
+ --color-sky-100: oklch(39.1% 0.09 240.876);
202
+ --color-sky-200: oklch(44.3% 0.11 240.79);
203
+ --color-sky-300: oklch(50% 0.134 242.749);
204
+ --color-sky-400: oklch(58.8% 0.158 241.966);
205
+ --color-slate-50: oklch(12.9% 0.042 264.695);
206
+ --color-slate-100: oklch(20.8% 0.042 265.755);
207
+ --color-slate-200: oklch(27.9% 0.041 260.031);
208
+ --color-slate-300: oklch(37.2% 0.044 257.287);
209
+ --color-slate-400: oklch(44.6% 0.043 257.281);
210
+ --color-violet-50: oklch(28.3% 0.141 291.089);
211
+ --color-violet-100: oklch(38% 0.189 293.745);
212
+ --color-violet-200: oklch(43.2% 0.232 292.759);
213
+ --color-violet-300: oklch(49.1% 0.27 292.581);
214
+ --color-violet-400: oklch(54.1% 0.281 293.009);
215
+ }
216
+
217
+ /* Style one-off values that the token and palette mirror cannot cover. */
218
+ @custom-variant light (&:where([data-theme="light"], [data-theme="light"] *));
219
+
111
220
  @layer base {
112
221
  :root {
113
222
  color-scheme: var(--dashboard-color-scheme, dark);
package/src/theme.ts ADDED
@@ -0,0 +1,49 @@
1
+ /** Theme the dashboard shows: the saved choice, or the system color scheme. */
2
+ export type DashboardThemeChoice = "dark" | "light" | "system";
3
+
4
+ /** Browser storage key for the theme choice. No value means `system`. */
5
+ export const DASHBOARD_THEME_STORAGE_KEY = "junior-dashboard-theme";
6
+
7
+ /** Window event that tells the theme script to apply a new saved choice. */
8
+ export const DASHBOARD_THEME_CHANGE_EVENT = "junior:dashboard-theme";
9
+
10
+ /** Browser chrome color for each theme. Dark is the default. */
11
+ export const DASHBOARD_THEME_COLORS = {
12
+ dark: "#000000",
13
+ light: "#ffffff",
14
+ } as const;
15
+
16
+ /**
17
+ * Inline head script that sets `data-theme` on `<html>` before first paint.
18
+ * It also follows system color scheme changes, choices from other tabs, and
19
+ * the change event, so it owns every theme update after page load.
20
+ */
21
+ export const DASHBOARD_THEME_SCRIPT = `(function () {
22
+ var media = window.matchMedia("(prefers-color-scheme: light)");
23
+ function apply() {
24
+ var choice = null;
25
+ try {
26
+ choice = window.localStorage.getItem(${JSON.stringify(DASHBOARD_THEME_STORAGE_KEY)});
27
+ } catch (_error) {}
28
+ var theme =
29
+ choice === "light" || choice === "dark"
30
+ ? choice
31
+ : media.matches
32
+ ? "light"
33
+ : "dark";
34
+ document.documentElement.dataset.theme = theme;
35
+ var meta = document.querySelector('meta[name="theme-color"]');
36
+ if (meta) {
37
+ meta.setAttribute(
38
+ "content",
39
+ theme === "light" ? ${JSON.stringify(DASHBOARD_THEME_COLORS.light)} : ${JSON.stringify(DASHBOARD_THEME_COLORS.dark)},
40
+ );
41
+ }
42
+ }
43
+ apply();
44
+ media.addEventListener("change", apply);
45
+ window.addEventListener("storage", function (event) {
46
+ if (event.key === null || event.key === ${JSON.stringify(DASHBOARD_THEME_STORAGE_KEY)}) apply();
47
+ });
48
+ window.addEventListener(${JSON.stringify(DASHBOARD_THEME_CHANGE_EVENT)}, apply);
49
+ })();`;
@@ -1,18 +0,0 @@
1
- import type { BaselineSnapshotReport } from "@sentry/junior/api/schema";
2
-
3
- import { SnapshotSummary } from "./SnapshotSummary";
4
-
5
- /** Show the install-wide baseline Sandbox snapshot used without a Workspace. */
6
- export function BaselineSnapshotCard(props: {
7
- snapshot: BaselineSnapshotReport | null;
8
- }) {
9
- return (
10
- <SnapshotSummary
11
- description="Default Sandboxes start from this install-wide snapshot when no Workspace is selected."
12
- emptyDescription="No baseline snapshot is registered yet. The first base Sandbox prepare creates one."
13
- headingId="baseline-snapshot-heading"
14
- snapshot={props.snapshot}
15
- title="Baseline snapshot"
16
- />
17
- );
18
- }