@sentry/junior-dashboard 0.266.0 → 0.268.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 (96) hide show
  1. package/dist/app.js +85 -12
  2. package/dist/client.js +94 -64
  3. package/dist/index.js +85 -12
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -5
  6. package/src/app.ts +0 -2
  7. package/src/auth.ts +4 -7
  8. package/src/client/App.tsx +55 -53
  9. package/src/client/components/DetailsPanel.tsx +12 -13
  10. package/src/client/components/Drawer.tsx +46 -23
  11. package/src/client/components/ImageAttachment.tsx +1 -1
  12. package/src/client/components/ParticipantAvatarStack.tsx +6 -2
  13. package/src/client/components/README.md +15 -2
  14. package/src/client/components/UserInputForm.tsx +283 -0
  15. package/src/client/components/layout/DashboardHeader.tsx +13 -0
  16. package/src/client/components/ui/alert.tsx +1 -1
  17. package/src/client/components/ui/button-group.tsx +59 -0
  18. package/src/client/components/ui/button.tsx +11 -1
  19. package/src/client/components/ui/dialog.tsx +29 -35
  20. package/src/client/components/ui/drawer.tsx +140 -0
  21. package/src/client/components/ui/empty.tsx +30 -1
  22. package/src/client/components/ui/separator.tsx +22 -0
  23. package/src/client/components/ui/useSheet.ts +5 -1
  24. package/src/client/conversations/ComposerImages.tsx +66 -14
  25. package/src/client/conversations/ConversationComposer.tsx +84 -12
  26. package/src/client/conversations/ConversationDetails.tsx +3 -27
  27. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  28. package/src/client/conversations/ConversationHomeList.tsx +6 -0
  29. package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
  30. package/src/client/conversations/ConversationPage.tsx +160 -2
  31. package/src/client/conversations/ConversationSidebar.tsx +6 -0
  32. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  33. package/src/client/conversations/ConversationWorkspace.tsx +4 -0
  34. package/src/client/conversations/DetailsList.tsx +33 -0
  35. package/src/client/conversations/EventDetails.tsx +46 -19
  36. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  37. package/src/client/conversations/PendingMailboxStack.tsx +310 -112
  38. package/src/client/conversations/README.md +30 -13
  39. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  40. package/src/client/conversations/TranscriptActivityGroup.tsx +27 -19
  41. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  42. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  43. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  44. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  45. package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
  46. package/src/client/conversations/TranscriptMessageView.tsx +10 -1
  47. package/src/client/conversations/TranscriptSummary.tsx +5 -31
  48. package/src/client/conversations/TranscriptToolView.tsx +13 -1
  49. package/src/client/conversations/TranscriptTurnContextView.tsx +174 -161
  50. package/src/client/conversations/TranscriptView.tsx +7 -3
  51. package/src/client/conversations/UserInputRequestCard.tsx +39 -0
  52. package/src/client/conversations/conversationOutbox.ts +46 -0
  53. package/src/client/conversations/eventTime.ts +13 -1
  54. package/src/client/conversations/eventTranscript.ts +12 -5
  55. package/src/client/conversations/queries.ts +127 -26
  56. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  57. package/src/client/conversations/turnContext.ts +15 -0
  58. package/src/client/conversations/userInputRequest.ts +56 -0
  59. package/src/client/format.ts +31 -8
  60. package/src/client/pages/SettingsPage.tsx +103 -3
  61. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  62. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  63. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  64. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  65. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  66. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  67. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  68. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  69. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  70. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  71. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  72. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  73. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  74. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  75. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  76. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  77. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  78. package/src/client/pages/automations/README.md +56 -46
  79. package/src/client/pages/code/RepositorySections.tsx +1 -0
  80. package/src/client/pages/code/RepositoryTabs.tsx +3 -0
  81. package/src/client/primaryNav.ts +15 -2
  82. package/src/client/review-desk/ConversationReviewBar.tsx +62 -0
  83. package/src/client/review-desk/ReviewDeskPage.tsx +277 -0
  84. package/src/client/review-desk/ReviewItemActions.tsx +165 -0
  85. package/src/client/review-desk/ReviewItemCard.tsx +47 -0
  86. package/src/client/review-desk/ReviewItemDetail.tsx +115 -0
  87. package/src/client/review-desk/queries.ts +164 -0
  88. package/src/client/review-desk/reviewItemFormat.ts +61 -0
  89. package/src/client/types.ts +3 -0
  90. package/src/mock-reporting/fixtures.ts +54 -2
  91. package/src/mock-reporting/routes.ts +20 -0
  92. package/src/shell.ts +3 -0
  93. package/src/client/conversations/transcriptStyles.ts +0 -10
  94. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  95. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  96. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.266.0",
3
+ "version": "0.268.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -29,16 +29,20 @@
29
29
  "cn": "^0.5.0",
30
30
  "hono": "^4.13.11",
31
31
  "lucide-react": "^1.17.0",
32
- "nitro": "3.0.260522-beta",
33
32
  "react": "^19.2.6",
34
33
  "react-dom": "^19.2.6",
35
34
  "react-is": "^19.2.6",
35
+ "react-markdown": "^10.1.0",
36
36
  "react-router": "^7.18.2",
37
37
  "recharts": "3.8.0",
38
+ "remark-breaks": "^4.0.0",
39
+ "remark-gfm": "^4.0.1",
38
40
  "shiki": "4.1.0",
39
41
  "zod": "^4.5.4",
40
- "@sentry/junior": "0.266.0",
41
- "@sentry/junior-plugin-api": "0.266.0"
42
+ "@sentry/junior-plugin-api": "0.268.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@sentry/junior": "0.268.0"
42
46
  },
43
47
  "devDependencies": {
44
48
  "@oxlint/plugins": "1.79.0",
@@ -49,6 +53,7 @@
49
53
  "@types/react-dom": "^19.2.3",
50
54
  "@vitest/coverage-v8": "4.1.12",
51
55
  "eslint": "^10.8.0",
56
+ "nitro": "3.0.260522-beta",
52
57
  "oxlint": "^1.79.0",
53
58
  "shadcn": "4.23.0",
54
59
  "tailwindcss": "^4.3.0",
@@ -56,7 +61,8 @@
56
61
  "tsup": "^8.5.1",
57
62
  "tw-animate-css": "^1.4.0",
58
63
  "typescript": "^7.0.2",
59
- "vitest": "^4.1.12"
64
+ "vitest": "^4.1.12",
65
+ "@sentry/junior": "0.268.0"
60
66
  },
61
67
  "scripts": {
62
68
  "build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
package/src/app.ts CHANGED
@@ -126,8 +126,6 @@ function resolveDashboardOptions(
126
126
  ): DashboardRuntimeOptions {
127
127
  return {
128
128
  ...options,
129
- authRequired:
130
- options.authRequired ?? readEnvFlag("JUNIOR_DASHBOARD_AUTH_REQUIRED"),
131
129
  mockConversations:
132
130
  options.mockConversations ??
133
131
  readEnvFlag("JUNIOR_DASHBOARD_MOCK_CONVERSATIONS"),
package/src/auth.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { isAllowedSignInEmail } from "@sentry/junior/api";
1
2
  import { dashboardSessionSchema } from "./api/schema";
2
3
  import type { DashboardSession } from "./api/schema";
3
4
 
@@ -34,13 +35,9 @@ export function dashboardSessionIsAuthorized(
34
35
  session: DashboardSession,
35
36
  allowedDomains: string[],
36
37
  ): boolean {
37
- const email = session.user.email.toLowerCase();
38
- const separator = email.lastIndexOf("@");
39
- const domain = separator > 0 ? email.slice(separator + 1) : undefined;
40
- return Boolean(
41
- session.user.emailVerified &&
42
- domain !== undefined &&
43
- allowedDomains.includes(domain),
38
+ return (
39
+ session.user.emailVerified === true &&
40
+ isAllowedSignInEmail(session.user.email, allowedDomains)
44
41
  );
45
42
  }
46
43
 
@@ -1,6 +1,5 @@
1
- import { AutomationEditPage } from "./pages/automations/AutomationEditPage";
2
1
  import { useEffect, useMemo, useState } from "react";
3
- import { Navigate, Route, Routes, useLocation } from "react-router";
2
+ import { Navigate, Route, Routes, useLocation, useParams } from "react-router";
4
3
 
5
4
  import {
6
5
  useConversationsData,
@@ -21,6 +20,7 @@ import {
21
20
  import { DashboardHeader } from "./components/layout/DashboardHeader";
22
21
  import { VisualViewportShell } from "./components/layout/VisualViewportShell";
23
22
  import {
23
+ automationPath,
24
24
  buildConversations,
25
25
  conversationDetailTitle,
26
26
  conversationDisplayTitle,
@@ -52,9 +52,8 @@ import {
52
52
  isMemoryLibraryPath,
53
53
  MemoryRouteLoading,
54
54
  } from "./pages/memory/MemoryPageLayout";
55
- import { AutomationExecutionsPage } from "./pages/automations/AutomationExecutionsPage";
55
+ import { AutomationPage } from "./pages/automations/AutomationPage";
56
56
  import { AutomationRunsPage } from "./pages/automations/AutomationRunsPage";
57
- import { AutomationVersionsPage } from "./pages/automations/AutomationVersionsPage";
58
57
  import { AutomationsPage } from "./pages/automations/AutomationsPage";
59
58
  import {
60
59
  AutomationsPageLayout,
@@ -67,6 +66,8 @@ import {
67
66
  } from "./pages/user/PluginUserPage";
68
67
  import { AdminPage } from "./pages/admin/AdminPage";
69
68
  import { buildPrimaryNavItems } from "./primaryNav";
69
+ import { ReviewDeskPage } from "./review-desk/ReviewDeskPage";
70
+ import { useReviewItemCountData } from "./review-desk/queries";
70
71
  import type { DashboardCoreData } from "./types";
71
72
 
72
73
  const dashboardNoise = {
@@ -112,11 +113,13 @@ export function DashboardShell() {
112
113
  ? (conversationDetailTitle(conversationDetail.data) ??
113
114
  conversationDisplayTitle(mobileConversation))
114
115
  : undefined;
116
+ const reviewCountQuery = useReviewItemCountData(loggedIn);
115
117
  const primaryNavItems = buildPrimaryNavItems({
116
118
  loading,
117
119
  loggedIn,
118
120
  pathname: location.pathname,
119
121
  primaryUserPages,
122
+ reviewCount: reviewCountQuery.data?.mine,
120
123
  userPagesPending: userPagesQuery.isPending,
121
124
  });
122
125
 
@@ -207,48 +210,40 @@ export function DashboardShell() {
207
210
  <Routes>
208
211
  <Route element={<LegacyAutomationsRedirect />} path="/tasks" />
209
212
  <Route element={<LegacyAutomationsRedirect />} path="/tasks/*" />
213
+ <Route
214
+ element={<LegacyAutomationExecutionsRedirect />}
215
+ path="/automations/:automationId/executions"
216
+ />
210
217
  <Route
211
218
  element={
212
- loading ? (
213
- <AutomationsPageLayout>
214
- <AutomationsRouteLoading
215
- description="Terminal runs for one scheduled or event automation."
216
- label="Loading automation executions"
217
- title="Automation executions"
218
- variant="list"
219
- />
220
- </AutomationsPageLayout>
221
- ) : loggedIn ? (
222
- <AutomationsPageLayout>
223
- <AutomationExecutionsPage enabled={loggedIn} />
224
- </AutomationsPageLayout>
219
+ loading || loggedIn ? (
220
+ <AutomationPage enabled={loggedIn} tab="runs" />
225
221
  ) : (
226
222
  <Navigate replace to="/" />
227
223
  )
228
224
  }
229
- path="/automations/:automationId/executions"
225
+ path="/automations/:automationId/runs"
230
226
  />
231
227
  <Route
232
228
  element={
233
- loading ? (
234
- <AutomationsPageLayout>
235
- <AutomationsRouteLoading
236
- description="Saved settings for this automation, newest first."
237
- label="Loading version history"
238
- title="Version history"
239
- variant="list"
240
- />
241
- </AutomationsPageLayout>
242
- ) : loggedIn ? (
243
- <AutomationsPageLayout>
244
- <AutomationVersionsPage enabled={loggedIn} />
245
- </AutomationsPageLayout>
229
+ loading || loggedIn ? (
230
+ <AutomationPage enabled={loggedIn} tab="versions" />
246
231
  ) : (
247
232
  <Navigate replace to="/" />
248
233
  )
249
234
  }
250
235
  path="/automations/:automationId/versions"
251
236
  />
237
+ <Route
238
+ element={
239
+ loading || loggedIn ? (
240
+ <AutomationPage enabled={loggedIn} tab="edit" />
241
+ ) : (
242
+ <Navigate replace to="/" />
243
+ )
244
+ }
245
+ path="/automations/:automationId/edit"
246
+ />
252
247
  <Route
253
248
  element={
254
249
  loading ? (
@@ -292,30 +287,9 @@ export function DashboardShell() {
292
287
  path="/automations/list"
293
288
  />
294
289
  <Route
295
- path="/automations/:automationId/edit"
296
290
  element={
297
291
  loading || loggedIn ? (
298
- <AutomationEditPage enabled={loggedIn} />
299
- ) : (
300
- <Navigate replace to="/" />
301
- )
302
- }
303
- />
304
- <Route
305
- element={
306
- loading ? (
307
- <AutomationsPageLayout>
308
- <AutomationsRouteLoading
309
- description="Find and manage automations across your linked workspaces."
310
- label="Loading automations"
311
- title="All automations"
312
- variant="list"
313
- />
314
- </AutomationsPageLayout>
315
- ) : loggedIn ? (
316
- <AutomationsPageLayout>
317
- <AutomationsPage enabled={loggedIn} view="list" />
318
- </AutomationsPageLayout>
292
+ <AutomationPage enabled={loggedIn} tab="overview" />
319
293
  ) : (
320
294
  <Navigate replace to="/" />
321
295
  )
@@ -392,6 +366,22 @@ export function DashboardShell() {
392
366
  }
393
367
  path="/"
394
368
  />
369
+ <Route
370
+ element={
371
+ loading ? (
372
+ <PageRouteLoading
373
+ description="Items that need a person, and who can act on them."
374
+ label="Loading review desk"
375
+ title="Review desk"
376
+ />
377
+ ) : loggedIn ? (
378
+ <ReviewDeskPage />
379
+ ) : (
380
+ <Navigate replace to="/" />
381
+ )
382
+ }
383
+ path="/review"
384
+ />
395
385
  <Route element={<CodePage />} path="/code" />
396
386
  {codeRepositoryTabs.map((tab) => (
397
387
  <Route
@@ -661,6 +651,18 @@ function LegacyAutomationsRedirect() {
661
651
  );
662
652
  }
663
653
 
654
+ /** Execution history became the Runs tab of the Automation page. */
655
+ function LegacyAutomationExecutionsRedirect() {
656
+ const location = useLocation();
657
+ const { automationId } = useParams();
658
+ return (
659
+ <Navigate
660
+ replace
661
+ to={`${automationPath(automationId ?? "", "runs")}${location.search}${location.hash}`}
662
+ />
663
+ );
664
+ }
665
+
664
666
  function LegacySystemRedirect(props: { section: "locations" | "people" }) {
665
667
  const location = useLocation();
666
668
  const legacyPrefix = `/${props.section}`;
@@ -110,20 +110,19 @@ export function DetailsPanel(props: {
110
110
  className="grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] border-l border-dashboard-border bg-dashboard-bg-elevated"
111
111
  >
112
112
  <header className="flex items-center justify-between gap-3 border-b border-dashboard-border py-2 pl-5 pr-3">
113
- <div className="flex min-w-0 items-center gap-3">
114
- <div className="min-w-0">{props.title}</div>
115
- {props.actions ? (
116
- <div className="flex items-center gap-0.5">{props.actions}</div>
117
- ) : null}
113
+ <div className="min-w-0 flex-1">{props.title}</div>
114
+ {/* Keep actions beside close, so they do not move with the title width. */}
115
+ <div className="flex shrink-0 items-center gap-0.5">
116
+ {props.actions}
117
+ <Button
118
+ ref={closeRef}
119
+ aria-label={closeLabel}
120
+ onClick={onClose}
121
+ size="icon"
122
+ >
123
+ <X aria-hidden="true" size={15} />
124
+ </Button>
118
125
  </div>
119
- <Button
120
- ref={closeRef}
121
- aria-label={closeLabel}
122
- onClick={onClose}
123
- size="icon"
124
- >
125
- <X aria-hidden="true" size={15} />
126
- </Button>
127
126
  </header>
128
127
  <div className="min-h-0 overflow-y-auto overscroll-contain">
129
128
  {props.children}
@@ -1,20 +1,31 @@
1
+ import { X } from "lucide-react";
1
2
  import { useRef, type ReactNode } from "react";
2
3
 
3
4
  import { cn } from "../styles";
4
- import { Sheet, SheetContent, SheetHeader } from "./ui/sheet";
5
+ import { Button } from "./ui/button";
6
+ import {
7
+ Drawer as DrawerRoot,
8
+ DrawerClose,
9
+ DrawerContent as DrawerPopup,
10
+ } from "./ui/drawer";
5
11
  import { useSheet } from "./ui/useSheet";
6
12
 
7
13
  type DrawerProps = {
8
14
  actions?: ReactNode;
9
15
  children: ReactNode;
10
16
  closeLabel: string;
17
+ /** Remove the body inset, so content such as the event log meets the edges. */
18
+ flush?: boolean;
11
19
  header: ReactNode;
12
20
  onClose(): void;
13
21
  openKey: string;
14
22
  width?: "default" | "wide" | "narrow";
15
23
  } & ({ titleId: string; label?: never } | { label: string; titleId?: never });
16
24
 
17
- /** Render a side drawer with shared modal focus and dismissal behavior. */
25
+ /**
26
+ * Render a side drawer with shared modal focus and dismissal behavior.
27
+ * A drawer opened from inside another drawer stacks in front of it.
28
+ */
18
29
  export function Drawer(props: DrawerProps) {
19
30
  return <DrawerContent key={props.openKey} {...props} />;
20
31
  }
@@ -23,37 +34,49 @@ function DrawerContent(props: DrawerProps) {
23
34
  const closeRef = useRef<HTMLButtonElement>(null);
24
35
  const sheet = useSheet(props.onClose, closeRef);
25
36
  return (
26
- <Sheet {...sheet.rootProps}>
27
- <SheetContent
37
+ <DrawerRoot {...sheet.rootProps} swipeDirection="right">
38
+ <DrawerPopup
28
39
  {...sheet.contentProps}
29
40
  aria-label={props.label}
30
41
  aria-labelledby={props.titleId}
31
- side="right"
32
- closeButtonProps={{
33
- ref: closeRef,
34
- "aria-label": props.closeLabel,
35
- className:
36
- "right-4 top-[max(0.75rem,env(safe-area-inset-top))] md:right-5",
37
- }}
42
+ data-dashboard-modal=""
38
43
  className={cn(
39
- "grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)] gap-0 outline-none data-[side=right]:w-full data-[side=right]:sm:max-w-none",
44
+ "data-[swipe-axis=x]:[--drawer-content-width:100%] data-[swipe-axis=x]:sm:[--drawer-content-width:100%]",
40
45
  props.width === "wide"
41
- ? "data-[side=right]:md:w-[min(760px,94vw)]"
46
+ ? "data-[swipe-axis=x]:md:[--drawer-content-width:min(760px,94vw)]"
42
47
  : props.width === "narrow"
43
- ? "data-[side=right]:md:w-[min(440px,94vw)]"
44
- : "data-[side=right]:md:w-[min(560px,94vw)]",
48
+ ? "data-[swipe-axis=x]:md:[--drawer-content-width:min(440px,94vw)]"
49
+ : "data-[swipe-axis=x]:md:[--drawer-content-width:min(560px,94vw)]",
45
50
  )}
46
51
  >
47
- <SheetHeader className="flex-row items-start gap-3 border-b border-dashboard-border bg-dashboard-bg-elevated px-4 py-3 pr-14 pt-[max(0.75rem,env(safe-area-inset-top))] md:pl-5 md:pr-14">
52
+ <header className="flex shrink-0 items-start gap-3 border-b border-dashboard-border bg-dashboard-bg-elevated px-4 py-3 pt-[max(0.75rem,env(safe-area-inset-top))] md:px-5">
48
53
  <div className="min-w-0 flex-1">{props.header}</div>
49
- {props.actions ? (
50
- <div className="flex items-start gap-1.5">{props.actions}</div>
51
- ) : null}
52
- </SheetHeader>
53
- <div className="min-h-0 overflow-auto px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5">
54
+ <div className="flex shrink-0 items-start gap-1.5">
55
+ {props.actions}
56
+ <DrawerClose
57
+ render={
58
+ <Button
59
+ ref={closeRef}
60
+ aria-label={props.closeLabel}
61
+ size="icon"
62
+ />
63
+ }
64
+ >
65
+ <X aria-hidden="true" size={15} strokeWidth={2.25} />
66
+ </DrawerClose>
67
+ </div>
68
+ </header>
69
+ <div
70
+ className={cn(
71
+ "min-h-0 flex-1 overflow-auto",
72
+ props.flush
73
+ ? "pb-[env(safe-area-inset-bottom)]"
74
+ : "px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-5",
75
+ )}
76
+ >
54
77
  {props.children}
55
78
  </div>
56
- </SheetContent>
57
- </Sheet>
79
+ </DrawerPopup>
80
+ </DrawerRoot>
58
81
  );
59
82
  }
@@ -98,7 +98,7 @@ function ImagePreview(props: {
98
98
  <Dialog {...sheet.rootProps}>
99
99
  <DialogContent
100
100
  {...sheet.contentProps}
101
- className="top-0 left-0 h-dvh w-screen max-w-none translate-x-0 translate-y-0 gap-0 overflow-hidden rounded-none border-0 bg-dashboard-ink p-0 shadow-none sm:max-w-none md:top-1/2 md:left-1/2 md:h-[min(90dvh,960px)] md:w-[min(92vw,1440px)] md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-lg md:border"
101
+ className="top-0 left-0 h-dvh w-screen max-w-none translate-x-0 translate-y-0 gap-0 overflow-hidden rounded-none border-0 bg-dashboard-ink p-0 shadow-none max-h-none sm:top-0 sm:translate-y-0 md:top-1/2 md:left-1/2 md:h-[min(90dvh,960px)] md:w-[min(92vw,1440px)] md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-lg md:border"
102
102
  >
103
103
  <div className="grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)] bg-dashboard-bg-elevated">
104
104
  <header className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-1.5 border-b border-dashboard-border-strong bg-dashboard-surface-raised px-3 py-2 pt-[max(0.5rem,env(safe-area-inset-top))] md:px-4">
@@ -13,7 +13,8 @@ type ParticipantAvatarStackProps = {
13
13
  size: "detail" | "list";
14
14
  };
15
15
 
16
- function participantName(participant: ActorIdentity): string {
16
+ /** Name an actor for display: full name first, then handle, email, or ID. */
17
+ export function participantName(participant: ActorIdentity): string {
17
18
  return (
18
19
  participant.fullName?.trim() ||
19
20
  participant.slackUserName?.trim() ||
@@ -51,7 +52,8 @@ function ParticipantTooltipContent(props: { participant: ActorIdentity }) {
51
52
  );
52
53
  }
53
54
 
54
- function ParticipantAvatar(props: {
55
+ /** Show one actor avatar with their name and email on hover or focus. */
56
+ export function ParticipantAvatar(props: {
55
57
  participant: ActorIdentity;
56
58
  size: ParticipantAvatarStackProps["size"];
57
59
  }) {
@@ -62,6 +64,8 @@ function ParticipantAvatar(props: {
62
64
  render={
63
65
  <span
64
66
  aria-label={label}
67
+ // A name on a generic span is often skipped by screen readers.
68
+ role="img"
65
69
  className="relative -ml-1 inline-flex rounded-full first:ml-0 pointer-events-none focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus [@media(hover:hover)_and_(pointer:fine)]:pointer-events-auto"
66
70
  onKeyDown={(event) => event.stopPropagation()}
67
71
  tabIndex={0}
@@ -32,7 +32,8 @@ file when every caller needs the same style or variant. App components compose
32
32
 
33
33
  | App component | shadcn source |
34
34
  | ------------------------------ | ----------------- |
35
- | `Drawer`, `BottomSheet` | `ui/sheet` |
35
+ | `Drawer` | `ui/drawer` |
36
+ | `BottomSheet` | `ui/sheet` |
36
37
  | `layout/DashboardHeader` (nav) | `ui/sheet` |
37
38
  | `ProfileMenu` (desktop) | `ui/popover` |
38
39
  | `ActorAvatar` | `ui/avatar` |
@@ -62,6 +63,9 @@ Rules for adopted components:
62
63
  - Use `ui/alert-dialog` for every confirmation. Do not use `window.confirm`.
63
64
  Keep the confirmed item in state after close, so the closing dialog keeps its
64
65
  text.
66
+ - Use `ui/dialog` for a short task form, such as the fork message. Do not put
67
+ a form in a `Drawer` or a `DetailsPanel`. On small screens the dialog sits
68
+ near the top, so the on-screen keyboard does not cover its fields.
65
69
  - Use `useSheet` for each app `ui/dialog`, as for sheets. It shares the
66
70
  document scroll lock. Only the front modal closes on Escape or a backdrop
67
71
  tap. Mount the dialog only while it is open.
@@ -86,6 +90,10 @@ Rules for adopted components:
86
90
  - For a link that looks like a button, put `buttonVariants()` on the router
87
91
  `Link`. A form page has one `Button variant="primary"` for its main save
88
92
  action.
93
+ - Use `Button variant="quiet"` for a secondary action beside the primary one,
94
+ such as Dismiss. It has no border, so the primary action stays dominant.
95
+ - In a form, show each choice as a whole bordered row around `RadioGroupItem`
96
+ or `Checkbox`. The row shows hover and selected state. See `UserInputForm`.
89
97
 
90
98
  ## Foundations
91
99
 
@@ -149,7 +157,12 @@ Shared class strings live in `src/client/styles.ts`.
149
157
  child. Use `ui/accordion` for a set of sections with headings.
150
158
  - **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
151
159
  `BottomSheet` below that width. It owns Escape and the scroll frame for
152
- docked panels.
160
+ docked panels. Docked actions sit beside the close control, so their
161
+ position does not change with the title width.
162
+ - **Drawer.** `Drawer` is a right-side `ui/drawer`. A `Drawer` opened from
163
+ inside another `Drawer` stacks in front of it. The parent scales back,
164
+ shows a thin edge behind the child, and hides its content. Set `flush` when
165
+ the content owns its insets and sticky headers, such as the event log.
153
166
  - **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
154
167
  Deployment, and Item icons. Do not replace it with a provider logo or a
155
168
  status-only icon.