@sentry/junior-dashboard 0.266.0 → 0.267.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/dist/app.js +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/ImageAttachment.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +4 -1
- package/src/client/components/README.md +11 -2
- package/src/client/components/ui/alert.tsx +1 -1
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/dialog.tsx +29 -35
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +30 -1
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/DetailsList.tsx +33 -0
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +310 -112
- package/src/client/conversations/README.md +30 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptSummary.tsx +5 -31
- package/src/client/conversations/TranscriptTurnContextView.tsx +33 -18
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- 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.
|
|
3
|
+
"version": "0.267.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.
|
|
41
|
-
|
|
42
|
+
"@sentry/junior-plugin-api": "0.267.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@sentry/junior": "0.267.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.267.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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
package/src/client/App.tsx
CHANGED
|
@@ -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 {
|
|
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,
|
|
@@ -207,48 +206,40 @@ export function DashboardShell() {
|
|
|
207
206
|
<Routes>
|
|
208
207
|
<Route element={<LegacyAutomationsRedirect />} path="/tasks" />
|
|
209
208
|
<Route element={<LegacyAutomationsRedirect />} path="/tasks/*" />
|
|
209
|
+
<Route
|
|
210
|
+
element={<LegacyAutomationExecutionsRedirect />}
|
|
211
|
+
path="/automations/:automationId/executions"
|
|
212
|
+
/>
|
|
210
213
|
<Route
|
|
211
214
|
element={
|
|
212
|
-
loading ? (
|
|
213
|
-
<
|
|
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>
|
|
215
|
+
loading || loggedIn ? (
|
|
216
|
+
<AutomationPage enabled={loggedIn} tab="runs" />
|
|
225
217
|
) : (
|
|
226
218
|
<Navigate replace to="/" />
|
|
227
219
|
)
|
|
228
220
|
}
|
|
229
|
-
path="/automations/:automationId/
|
|
221
|
+
path="/automations/:automationId/runs"
|
|
230
222
|
/>
|
|
231
223
|
<Route
|
|
232
224
|
element={
|
|
233
|
-
loading ? (
|
|
234
|
-
<
|
|
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>
|
|
225
|
+
loading || loggedIn ? (
|
|
226
|
+
<AutomationPage enabled={loggedIn} tab="versions" />
|
|
246
227
|
) : (
|
|
247
228
|
<Navigate replace to="/" />
|
|
248
229
|
)
|
|
249
230
|
}
|
|
250
231
|
path="/automations/:automationId/versions"
|
|
251
232
|
/>
|
|
233
|
+
<Route
|
|
234
|
+
element={
|
|
235
|
+
loading || loggedIn ? (
|
|
236
|
+
<AutomationPage enabled={loggedIn} tab="edit" />
|
|
237
|
+
) : (
|
|
238
|
+
<Navigate replace to="/" />
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
path="/automations/:automationId/edit"
|
|
242
|
+
/>
|
|
252
243
|
<Route
|
|
253
244
|
element={
|
|
254
245
|
loading ? (
|
|
@@ -292,30 +283,9 @@ export function DashboardShell() {
|
|
|
292
283
|
path="/automations/list"
|
|
293
284
|
/>
|
|
294
285
|
<Route
|
|
295
|
-
path="/automations/:automationId/edit"
|
|
296
286
|
element={
|
|
297
287
|
loading || loggedIn ? (
|
|
298
|
-
<
|
|
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>
|
|
288
|
+
<AutomationPage enabled={loggedIn} tab="overview" />
|
|
319
289
|
) : (
|
|
320
290
|
<Navigate replace to="/" />
|
|
321
291
|
)
|
|
@@ -661,6 +631,18 @@ function LegacyAutomationsRedirect() {
|
|
|
661
631
|
);
|
|
662
632
|
}
|
|
663
633
|
|
|
634
|
+
/** Execution history became the Runs tab of the Automation page. */
|
|
635
|
+
function LegacyAutomationExecutionsRedirect() {
|
|
636
|
+
const location = useLocation();
|
|
637
|
+
const { automationId } = useParams();
|
|
638
|
+
return (
|
|
639
|
+
<Navigate
|
|
640
|
+
replace
|
|
641
|
+
to={`${automationPath(automationId ?? "", "runs")}${location.search}${location.hash}`}
|
|
642
|
+
/>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
664
646
|
function LegacySystemRedirect(props: { section: "locations" | "people" }) {
|
|
665
647
|
const location = useLocation();
|
|
666
648
|
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="
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
<
|
|
27
|
-
<
|
|
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
|
-
|
|
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
|
-
"
|
|
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-[
|
|
46
|
+
? "data-[swipe-axis=x]:md:[--drawer-content-width:min(760px,94vw)]"
|
|
42
47
|
: props.width === "narrow"
|
|
43
|
-
? "data-[
|
|
44
|
-
: "data-[
|
|
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
|
-
<
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
</
|
|
57
|
-
</
|
|
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
|
|
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">
|
|
@@ -51,7 +51,8 @@ function ParticipantTooltipContent(props: { participant: ActorIdentity }) {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/** Show one actor avatar with their name and email on hover or focus. */
|
|
55
|
+
export function ParticipantAvatar(props: {
|
|
55
56
|
participant: ActorIdentity;
|
|
56
57
|
size: ParticipantAvatarStackProps["size"];
|
|
57
58
|
}) {
|
|
@@ -62,6 +63,8 @@ function ParticipantAvatar(props: {
|
|
|
62
63
|
render={
|
|
63
64
|
<span
|
|
64
65
|
aria-label={label}
|
|
66
|
+
// A name on a generic span is often skipped by screen readers.
|
|
67
|
+
role="img"
|
|
65
68
|
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
69
|
onKeyDown={(event) => event.stopPropagation()}
|
|
67
70
|
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
|
|
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.
|
|
@@ -149,7 +153,12 @@ Shared class strings live in `src/client/styles.ts`.
|
|
|
149
153
|
child. Use `ui/accordion` for a set of sections with headings.
|
|
150
154
|
- **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
|
|
151
155
|
`BottomSheet` below that width. It owns Escape and the scroll frame for
|
|
152
|
-
docked panels.
|
|
156
|
+
docked panels. Docked actions sit beside the close control, so their
|
|
157
|
+
position does not change with the title width.
|
|
158
|
+
- **Drawer.** `Drawer` is a right-side `ui/drawer`. A `Drawer` opened from
|
|
159
|
+
inside another `Drawer` stacks in front of it. The parent scales back,
|
|
160
|
+
shows a thin edge behind the child, and hides its content. Set `flush` when
|
|
161
|
+
the content owns its insets and sticky headers, such as the event log.
|
|
153
162
|
- **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
|
|
154
163
|
Deployment, and Item icons. Do not replace it with a provider logo or a
|
|
155
164
|
status-only icon.
|
|
@@ -10,7 +10,7 @@ const alertVariants = cva(
|
|
|
10
10
|
default:
|
|
11
11
|
"border-dashboard-border-strong bg-dashboard-surface-raised text-dashboard-text",
|
|
12
12
|
destructive:
|
|
13
|
-
"border-rose-300/
|
|
13
|
+
"border-rose-300/20 bg-rose-300/[0.08] text-rose-100 *:data-[slot=alert-description]:text-rose-100/70 *:[svg]:text-rose-300",
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
defaultVariants: {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import { cn } from "@dashboard/lib/utils";
|
|
3
|
+
|
|
4
|
+
import { Separator } from "@dashboard/components/ui/separator";
|
|
5
|
+
|
|
6
|
+
const buttonGroupVariants = cva(
|
|
7
|
+
"flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
orientation: {
|
|
11
|
+
horizontal:
|
|
12
|
+
"*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
|
|
13
|
+
vertical:
|
|
14
|
+
"flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
orientation: "horizontal",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** Join related buttons into one control and name it for assistive technology. */
|
|
24
|
+
function ButtonGroup({
|
|
25
|
+
className,
|
|
26
|
+
orientation,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
role="group"
|
|
32
|
+
data-slot="button-group"
|
|
33
|
+
data-orientation={orientation}
|
|
34
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Draw a divider between buttons in a group. */
|
|
41
|
+
function ButtonGroupSeparator({
|
|
42
|
+
className,
|
|
43
|
+
orientation = "vertical",
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
46
|
+
return (
|
|
47
|
+
<Separator
|
|
48
|
+
data-slot="button-group-separator"
|
|
49
|
+
orientation={orientation}
|
|
50
|
+
className={cn(
|
|
51
|
+
"relative self-stretch bg-dashboard-border data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { ButtonGroup, ButtonGroupSeparator };
|
|
@@ -1,66 +1,60 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
1
2
|
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
2
3
|
import { cn } from "@dashboard/lib/utils";
|
|
3
4
|
|
|
4
|
-
/** Own the open state and modal focus rules for a dialog. */
|
|
5
|
-
function Dialog(
|
|
5
|
+
/** Own the open state and modal focus rules for a centered dialog. */
|
|
6
|
+
function Dialog(props: DialogPrimitive.Root.Props) {
|
|
6
7
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
|
10
|
-
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
10
|
/** Close the dialog through Base UI dismissal and focus handling. */
|
|
14
|
-
function DialogClose(
|
|
11
|
+
function DialogClose(props: DialogPrimitive.Close.Props) {
|
|
15
12
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
className,
|
|
20
|
-
...props
|
|
21
|
-
}: DialogPrimitive.Backdrop.Props) {
|
|
22
|
-
return (
|
|
23
|
-
<DialogPrimitive.Backdrop
|
|
24
|
-
data-slot="dialog-overlay"
|
|
25
|
-
className={cn(
|
|
26
|
-
"fixed inset-0 isolate z-50 bg-dashboard-overlay transition-opacity duration-180 motion-reduce:transition-none data-ending-style:opacity-0 data-starting-style:opacity-0",
|
|
27
|
-
className,
|
|
28
|
-
)}
|
|
29
|
-
{...props}
|
|
30
|
-
/>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Show the dialog above a backdrop, centered in the viewport. */
|
|
15
|
+
/** Show a short task above the page. Small screens keep it near the top, clear of the keyboard. */
|
|
35
16
|
function DialogContent({ className, ...props }: DialogPrimitive.Popup.Props) {
|
|
36
17
|
return (
|
|
37
|
-
<
|
|
38
|
-
|
|
18
|
+
<DialogPrimitive.Portal data-slot="dialog-portal">
|
|
19
|
+
{/* Dim an open sheet too, so a dialog opened from it stands out. */}
|
|
20
|
+
<DialogPrimitive.Backdrop
|
|
21
|
+
forceRender
|
|
22
|
+
data-slot="dialog-overlay"
|
|
23
|
+
className="fixed inset-0 z-50 bg-dashboard-overlay transition-opacity duration-180 motion-reduce:transition-none data-ending-style:opacity-0 data-starting-style:opacity-0"
|
|
24
|
+
/>
|
|
39
25
|
<DialogPrimitive.Popup
|
|
40
|
-
data-slot="dialog-content"
|
|
41
26
|
data-dashboard-modal=""
|
|
27
|
+
data-slot="dialog-content"
|
|
42
28
|
className={cn(
|
|
43
|
-
"fixed top-
|
|
29
|
+
"fixed top-[max(1rem,env(safe-area-inset-top))] left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-md -translate-x-1/2 gap-4 overflow-y-auto rounded-xl border border-dashboard-border-emphasis bg-dashboard-surface-raised p-5 text-sm text-dashboard-text shadow-lg outline-none transition-[opacity,scale] duration-180 motion-reduce:transition-none data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 sm:top-1/2 sm:-translate-y-1/2",
|
|
44
30
|
className,
|
|
45
31
|
)}
|
|
46
32
|
{...props}
|
|
47
33
|
/>
|
|
48
|
-
</
|
|
34
|
+
</DialogPrimitive.Portal>
|
|
49
35
|
);
|
|
50
36
|
}
|
|
51
37
|
|
|
52
|
-
/**
|
|
38
|
+
/** Name the dialog task. */
|
|
53
39
|
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
54
40
|
return (
|
|
55
41
|
<DialogPrimitive.Title
|
|
56
42
|
data-slot="dialog-title"
|
|
57
|
-
className={cn(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
43
|
+
className={cn("m-0 font-display text-lg font-medium", className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Keep dialog actions together at the end of the dialog. */
|
|
50
|
+
function DialogFooter({ className, ...props }: ComponentProps<"div">) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="dialog-footer"
|
|
54
|
+
className={cn("flex flex-wrap justify-end gap-2", className)}
|
|
61
55
|
{...props}
|
|
62
56
|
/>
|
|
63
57
|
);
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
export { Dialog, DialogClose, DialogContent, DialogTitle };
|
|
60
|
+
export { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle };
|