@sentry/junior-dashboard 0.267.0 → 0.269.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/README.md +7 -105
- package/dist/app.js +47 -8
- package/dist/client.js +74 -78
- package/dist/index.js +47 -8
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/api/schema.ts +2 -1
- package/src/app.ts +11 -7
- package/src/client/App.tsx +20 -4
- package/src/client/api.ts +4 -19
- package/src/client/components/AGENTS.md +65 -0
- package/src/client/components/ActorAvatar.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +3 -1
- package/src/client/components/ProfileMenu.tsx +88 -29
- package/src/client/components/UserInputForm.tsx +283 -0
- package/src/client/components/charts/AGENTS.md +15 -0
- package/src/client/components/layout/DashboardHeader.tsx +13 -0
- package/src/client/components/ui/button.tsx +11 -1
- package/src/client/conversations/AGENTS.md +42 -0
- package/src/client/conversations/ConversationHomeList.tsx +6 -0
- package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
- package/src/client/conversations/ConversationPage.tsx +4 -27
- package/src/client/conversations/ConversationSidebar.tsx +6 -0
- package/src/client/conversations/ConversationWorkspace.tsx +4 -0
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
- package/src/client/conversations/TranscriptMessageView.tsx +3 -0
- package/src/client/conversations/TranscriptToolView.tsx +13 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +142 -144
- package/src/client/conversations/turnContext.ts +15 -0
- package/src/client/format.ts +28 -28
- package/src/client/pages/SettingsPage.tsx +103 -3
- package/src/client/pages/automations/AGENTS.md +34 -0
- package/src/client/pages/automations/AutomationActions.tsx +18 -20
- package/src/client/pages/automations/AutomationEditor.tsx +3 -4
- package/src/client/pages/automations/AutomationOverview.tsx +3 -3
- package/src/client/pages/automations/AutomationStatus.tsx +2 -2
- package/src/client/pages/code/RepositorySections.tsx +1 -0
- package/src/client/pages/code/RepositoryTabs.tsx +3 -0
- package/src/client/pages/system/AGENTS.md +12 -0
- package/src/client/primaryNav.ts +15 -2
- package/src/client/review-desk/ConversationReviewBar.tsx +68 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +297 -0
- package/src/client/review-desk/ReviewItemActions.tsx +165 -0
- package/src/client/review-desk/ReviewItemCard.tsx +47 -0
- package/src/client/review-desk/ReviewItemDetail.tsx +123 -0
- package/src/client/review-desk/queries.ts +173 -0
- package/src/client/review-desk/reviewItemFormat.ts +61 -0
- package/src/mock-reporting/fixtures.ts +10 -0
- package/src/mock-reporting/routes.ts +20 -0
- package/src/shell.ts +3 -0
- package/src/tailwind.css +1 -1
- package/src/client/components/README.md +0 -215
- package/src/client/components/charts/README.md +0 -38
- package/src/client/conversations/README.md +0 -159
- package/src/client/conversations/UserInputRequestCard.tsx +0 -244
- package/src/client/conversations/userInputRequest.ts +0 -56
- package/src/client/pages/automations/README.md +0 -97
- package/src/client/pages/system/README.md +0 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.269.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"remark-gfm": "^4.0.1",
|
|
40
40
|
"shiki": "4.1.0",
|
|
41
41
|
"zod": "^4.5.4",
|
|
42
|
-
"@sentry/junior-plugin-api": "0.
|
|
42
|
+
"@sentry/junior-plugin-api": "0.269.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@sentry/junior": "0.
|
|
45
|
+
"@sentry/junior": "0.269.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"tw-animate-css": "^1.4.0",
|
|
63
63
|
"typescript": "^7.0.2",
|
|
64
64
|
"vitest": "^4.1.12",
|
|
65
|
-
"@sentry/junior": "0.
|
|
65
|
+
"@sentry/junior": "0.269.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
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/api/schema.ts
CHANGED
|
@@ -18,6 +18,8 @@ export const dashboardIdentitySchema = z
|
|
|
18
18
|
.object({
|
|
19
19
|
user: dashboardSessionUserSchema
|
|
20
20
|
.extend({
|
|
21
|
+
/** Junior API path of the viewer's Slack photo, then Google photo. */
|
|
22
|
+
avatarUrl: z.string().optional(),
|
|
21
23
|
/** Junior-wide admin role. The server still checks it on every admin route. */
|
|
22
24
|
isAdmin: z.boolean(),
|
|
23
25
|
})
|
|
@@ -38,7 +40,6 @@ export const dashboardConfigSchema = z
|
|
|
38
40
|
authPath: z.string(),
|
|
39
41
|
basePath: z.string(),
|
|
40
42
|
sentryConversationLinks: z.boolean(),
|
|
41
|
-
timeZone: z.string().optional(),
|
|
42
43
|
version: z.string().min(1),
|
|
43
44
|
})
|
|
44
45
|
.strict();
|
package/src/app.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
authenticatePersonalToken,
|
|
5
5
|
createJuniorApi,
|
|
6
6
|
jsonResponse,
|
|
7
|
+
readViewerAvatarUrl,
|
|
7
8
|
resolveViewerUser,
|
|
8
9
|
updateViewerDisplayName,
|
|
9
10
|
type ConversationWorkQueue,
|
|
@@ -300,11 +301,6 @@ function mockViewerFromSession(session: DashboardSession) {
|
|
|
300
301
|
};
|
|
301
302
|
}
|
|
302
303
|
|
|
303
|
-
/** Return an operator-configured timezone override, or undefined to let each viewer's browser use its own local timezone. */
|
|
304
|
-
function dashboardTimeZone(): string | undefined {
|
|
305
|
-
return process.env.JUNIOR_TIMEZONE || undefined;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
304
|
function pluginRoutePrefix(pluginName: string): string {
|
|
309
305
|
return `/api/plugins/${pluginName}`;
|
|
310
306
|
}
|
|
@@ -565,15 +561,21 @@ export function createDashboardApp(
|
|
|
565
561
|
authPath,
|
|
566
562
|
basePath,
|
|
567
563
|
sentryConversationLinks: hasSentryConversationLinks(),
|
|
568
|
-
timeZone: dashboardTimeZone(),
|
|
569
564
|
version: JUNIOR_VERSION,
|
|
570
565
|
});
|
|
571
566
|
});
|
|
572
|
-
|
|
567
|
+
// Mock reporting has no SQL identities, so it has no profile photos.
|
|
568
|
+
const viewerAvatarUrl = async (userId: string | undefined) =>
|
|
569
|
+
userId && !options.mockConversations
|
|
570
|
+
? await readViewerAvatarUrl(userId)
|
|
571
|
+
: undefined;
|
|
572
|
+
app.get("/api/me", async (c) => {
|
|
573
573
|
const session = c.get("authSession");
|
|
574
574
|
const viewer = c.get("viewer");
|
|
575
|
+
const avatarUrl = await viewerAvatarUrl(viewer?.id);
|
|
575
576
|
return jsonResponse(dashboardIdentitySchema, {
|
|
576
577
|
user: {
|
|
578
|
+
...(avatarUrl ? { avatarUrl } : undefined),
|
|
577
579
|
email: session.user.email,
|
|
578
580
|
emailVerified: session.user.emailVerified,
|
|
579
581
|
isAdmin: viewer?.isAdmin === true,
|
|
@@ -632,8 +634,10 @@ export function createDashboardApp(
|
|
|
632
634
|
{ status: 404 },
|
|
633
635
|
);
|
|
634
636
|
}
|
|
637
|
+
const avatarUrl = await viewerAvatarUrl(updated.id);
|
|
635
638
|
return jsonResponse(dashboardIdentitySchema, {
|
|
636
639
|
user: {
|
|
640
|
+
...(avatarUrl ? { avatarUrl } : undefined),
|
|
637
641
|
email: session.user.email,
|
|
638
642
|
emailVerified: session.user.emailVerified,
|
|
639
643
|
isAdmin: updated.isAdmin === true,
|
package/src/client/App.tsx
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
buildConversations,
|
|
25
25
|
conversationDetailTitle,
|
|
26
26
|
conversationDisplayTitle,
|
|
27
|
-
setDashboardTimeZone,
|
|
28
27
|
} from "./format";
|
|
29
28
|
import { isNewConversationPath } from "./conversations/conversationRoutes";
|
|
30
29
|
import { useDashboardServerVersion } from "./dashboard-version";
|
|
@@ -66,6 +65,8 @@ import {
|
|
|
66
65
|
} from "./pages/user/PluginUserPage";
|
|
67
66
|
import { AdminPage } from "./pages/admin/AdminPage";
|
|
68
67
|
import { buildPrimaryNavItems } from "./primaryNav";
|
|
68
|
+
import { ReviewDeskPage } from "./review-desk/ReviewDeskPage";
|
|
69
|
+
import { useReviewItemCountData } from "./review-desk/queries";
|
|
69
70
|
import type { DashboardCoreData } from "./types";
|
|
70
71
|
|
|
71
72
|
const dashboardNoise = {
|
|
@@ -82,9 +83,6 @@ export function DashboardShell() {
|
|
|
82
83
|
const data = query.data;
|
|
83
84
|
const serverVersion = useDashboardServerVersion(data?.config.version);
|
|
84
85
|
const userPages = userPagesQuery.data ?? [];
|
|
85
|
-
if (data) {
|
|
86
|
-
setDashboardTimeZone(data.config.timeZone);
|
|
87
|
-
}
|
|
88
86
|
const loading = !data && !query.error;
|
|
89
87
|
const loggedIn = Boolean(data?.config.authRequired && data.me.user.email);
|
|
90
88
|
const personalSpendQuery = usePersonalSpendData(loggedIn);
|
|
@@ -111,11 +109,13 @@ export function DashboardShell() {
|
|
|
111
109
|
? (conversationDetailTitle(conversationDetail.data) ??
|
|
112
110
|
conversationDisplayTitle(mobileConversation))
|
|
113
111
|
: undefined;
|
|
112
|
+
const reviewCountQuery = useReviewItemCountData(loggedIn);
|
|
114
113
|
const primaryNavItems = buildPrimaryNavItems({
|
|
115
114
|
loading,
|
|
116
115
|
loggedIn,
|
|
117
116
|
pathname: location.pathname,
|
|
118
117
|
primaryUserPages,
|
|
118
|
+
reviewCount: reviewCountQuery.data?.needsViewer,
|
|
119
119
|
userPagesPending: userPagesQuery.isPending,
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -362,6 +362,22 @@ export function DashboardShell() {
|
|
|
362
362
|
}
|
|
363
363
|
path="/"
|
|
364
364
|
/>
|
|
365
|
+
<Route
|
|
366
|
+
element={
|
|
367
|
+
loading ? (
|
|
368
|
+
<PageRouteLoading
|
|
369
|
+
description="Items that need a person, and who can act on them."
|
|
370
|
+
label="Loading review desk"
|
|
371
|
+
title="Review desk"
|
|
372
|
+
/>
|
|
373
|
+
) : loggedIn ? (
|
|
374
|
+
<ReviewDeskPage />
|
|
375
|
+
) : (
|
|
376
|
+
<Navigate replace to="/" />
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
path="/review"
|
|
380
|
+
/>
|
|
365
381
|
<Route element={<CodePage />} path="/code" />
|
|
366
382
|
{codeRepositoryTabs.map((tab) => (
|
|
367
383
|
<Route
|
package/src/client/api.ts
CHANGED
|
@@ -33,23 +33,9 @@ import { fetchDashboardJson } from "./http";
|
|
|
33
33
|
import type { DashboardCoreData, SystemData } from "./types";
|
|
34
34
|
|
|
35
35
|
const dashboardMetadataStaleTimeMs = 5 * 60_000;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/** Schedule the next spend refresh from the age of the server-cached report. */
|
|
40
|
-
export function personalSpendRefreshDelay(
|
|
41
|
-
generatedAt: string | undefined,
|
|
42
|
-
nowMs = Date.now(),
|
|
43
|
-
): number {
|
|
44
|
-
if (!generatedAt) return PERSONAL_SPEND_REFRESH_MS;
|
|
45
|
-
const generatedAtMs = Date.parse(generatedAt);
|
|
46
|
-
if (!Number.isFinite(generatedAtMs)) return PERSONAL_SPEND_REFRESH_MS;
|
|
47
|
-
const ageMs = Math.max(0, nowMs - generatedAtMs);
|
|
48
|
-
return Math.max(
|
|
49
|
-
MIN_PERSONAL_SPEND_REFRESH_MS,
|
|
50
|
-
PERSONAL_SPEND_REFRESH_MS - ageMs,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
36
|
+
// The server reuses its cached report until the viewer's spend changes, so a
|
|
37
|
+
// short poll shows a finished Turn without a full aggregate query each time.
|
|
38
|
+
const PERSONAL_SPEND_REFRESH_MS = 10_000;
|
|
53
39
|
|
|
54
40
|
/** Fetch dashboard shell data shared across browser routes. */
|
|
55
41
|
export function useDashboardCoreData() {
|
|
@@ -314,8 +300,7 @@ export function usePersonalSpendData(enabled: boolean) {
|
|
|
314
300
|
"/api/people/me/spend",
|
|
315
301
|
signal,
|
|
316
302
|
),
|
|
317
|
-
refetchInterval:
|
|
318
|
-
personalSpendRefreshDelay(query.state.data?.generatedAt),
|
|
303
|
+
refetchInterval: PERSONAL_SPEND_REFRESH_MS,
|
|
319
304
|
retry: false,
|
|
320
305
|
staleTime: 0,
|
|
321
306
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Dashboard UI kit
|
|
2
|
+
|
|
3
|
+
Shared dashboard components live here. Feature pages compose them instead of
|
|
4
|
+
copying Tailwind blocks. Follow `policies/frontend-components.md`.
|
|
5
|
+
`pnpm dashboard-style:check` enforces the token rules.
|
|
6
|
+
|
|
7
|
+
## shadcn/ui
|
|
8
|
+
|
|
9
|
+
- Add components with
|
|
10
|
+
`pnpm --filter @sentry/junior-dashboard exec shadcn add <component>`. They go
|
|
11
|
+
to `ui/`.
|
|
12
|
+
- `ui/` files are shadcn source on dashboard tokens. Remove unused exports.
|
|
13
|
+
Change a `ui/` file when every caller needs the same style.
|
|
14
|
+
- Use `ui/` components directly. Add an app component only when it adds
|
|
15
|
+
structure or behavior, not to rename a `ui/` component or its props.
|
|
16
|
+
- Do not apply the CLI's global border or body resets. Keep the Tailwind
|
|
17
|
+
radius scale.
|
|
18
|
+
|
|
19
|
+
## Component rules
|
|
20
|
+
|
|
21
|
+
- Use `ui/alert-dialog` for every confirmation. Never use `window.confirm`.
|
|
22
|
+
- Use `ui/dialog` for a short task form. Never put a form in a `Drawer` or
|
|
23
|
+
`DetailsPanel`. Use `useSheet` for each dialog and mount it only while open.
|
|
24
|
+
- Use `ui/trace` for transcript activity rows. Put only inline elements in
|
|
25
|
+
the row button.
|
|
26
|
+
- Give an icon-only control a label with `TooltipContent size="compact"`.
|
|
27
|
+
Never use the native `title` attribute.
|
|
28
|
+
- Set `focusableWhenDisabled` on a `ui/button` tooltip trigger that can be
|
|
29
|
+
disabled.
|
|
30
|
+
- Build form fields from `ui/field`. Use the ids `<control id>-error` and
|
|
31
|
+
`<control id>-help`.
|
|
32
|
+
- Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
|
|
33
|
+
`aria-label`.
|
|
34
|
+
- `ui/alert` sets `role="alert"`. Pass `role="status"` for a message that is
|
|
35
|
+
not an error.
|
|
36
|
+
- A form page has one `Button variant="primary"`.
|
|
37
|
+
- `ObjectIcon` owns native object icons. Do not replace it with a provider
|
|
38
|
+
logo.
|
|
39
|
+
|
|
40
|
+
## Tokens
|
|
41
|
+
|
|
42
|
+
- Keep every color in a token in `src/tailwind.css`, so each theme can
|
|
43
|
+
redefine it. Use `light:` only for values that no token covers.
|
|
44
|
+
- Use `var(--color-*)` for chart series. Never use hex values.
|
|
45
|
+
- Do not use `white/*` or `black/*` fills, borders, or rings, or hex surface
|
|
46
|
+
colors. Shadows may use black.
|
|
47
|
+
- Use `outline-dashboard-focus` for keyboard focus only.
|
|
48
|
+
- Use the named type scale. Keep `text-2xs` for compact badges and chart
|
|
49
|
+
annotations.
|
|
50
|
+
- Labels use sentence case. Do not use uppercase or letter spacing.
|
|
51
|
+
|
|
52
|
+
## Navigation
|
|
53
|
+
|
|
54
|
+
- App navigation uses pills, page links use `SegmentedNav`, and panel
|
|
55
|
+
switches use `ui/tabs`. Keep the three levels distinct.
|
|
56
|
+
|
|
57
|
+
## Ownership
|
|
58
|
+
|
|
59
|
+
- Conversation mobile geometry has hard owners: `layout/VisualViewportShell`,
|
|
60
|
+
`conversations/ChatLayout`, and `conversations/ComposerDock`. Pages pass
|
|
61
|
+
children only. Do not add viewport, offset, or safe-area math in pages.
|
|
62
|
+
- Home and create must not use `ComposerDock`.
|
|
63
|
+
- Feature surfaces stay under `conversations/` or `pages/` until two real
|
|
64
|
+
callers share them. Add a named component on the second real use.
|
|
65
|
+
- Keep Tailwind classes on the owning component.
|
|
@@ -13,7 +13,8 @@ type ParticipantAvatarStackProps = {
|
|
|
13
13
|
size: "detail" | "list";
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
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() ||
|
|
@@ -70,6 +71,7 @@ export function ParticipantAvatar(props: {
|
|
|
70
71
|
tabIndex={0}
|
|
71
72
|
>
|
|
72
73
|
<ActorAvatar
|
|
74
|
+
imageUrl={props.participant.avatarUrl}
|
|
73
75
|
name={participantName(props.participant)}
|
|
74
76
|
size={props.size}
|
|
75
77
|
/>
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ShieldCheck,
|
|
9
9
|
UserRound,
|
|
10
10
|
} from "lucide-react";
|
|
11
|
-
import { useState } from "react";
|
|
11
|
+
import { useEffect, useRef, useState } from "react";
|
|
12
12
|
import { Link } from "react-router";
|
|
13
13
|
import type { PersonalSpendReport } from "@sentry/junior/api/schema";
|
|
14
14
|
import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
|
|
@@ -18,6 +18,7 @@ import { pluginUserPagePath } from "../pages/user/PluginUserPage";
|
|
|
18
18
|
import { cn, dashboardInteractiveTextClass } from "../styles";
|
|
19
19
|
import type { Identity } from "../types";
|
|
20
20
|
import { ThemeSwitcher } from "./ThemeSwitcher";
|
|
21
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|
21
22
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
22
23
|
|
|
23
24
|
type ProfileMenuProps = {
|
|
@@ -49,6 +50,83 @@ function initials(name: string | null | undefined, email: string): string {
|
|
|
49
50
|
return email.slice(0, 1).toUpperCase();
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** Show the viewer's photo, with initials while it loads or when there is none. */
|
|
54
|
+
function ProfileAvatar({
|
|
55
|
+
className,
|
|
56
|
+
user,
|
|
57
|
+
}: {
|
|
58
|
+
className?: string;
|
|
59
|
+
user: Identity["user"];
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<Avatar
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
className={cn("border-0 shadow-black/40", className)}
|
|
65
|
+
>
|
|
66
|
+
<AvatarImage alt="User avatar" src={user.avatarUrl} />
|
|
67
|
+
<AvatarFallback className="font-bold tracking-wide">
|
|
68
|
+
{initials(user.name, user.email!)}
|
|
69
|
+
</AvatarFallback>
|
|
70
|
+
</Avatar>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SPEND_COUNT_UP_MS = 700;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Show one spend total. When the total changes, count up from the last shown
|
|
78
|
+
* value and tint the figure until the count ends.
|
|
79
|
+
*/
|
|
80
|
+
function SpendAmount({ value }: { value?: number }) {
|
|
81
|
+
const [shown, setShown] = useState(value);
|
|
82
|
+
const [counting, setCounting] = useState(false);
|
|
83
|
+
const shownRef = useRef(value);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const from = shownRef.current;
|
|
87
|
+
if (
|
|
88
|
+
value === undefined ||
|
|
89
|
+
from === undefined ||
|
|
90
|
+
from === value ||
|
|
91
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
92
|
+
) {
|
|
93
|
+
shownRef.current = value;
|
|
94
|
+
setShown(value);
|
|
95
|
+
setCounting(false);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const startedAt = performance.now();
|
|
100
|
+
let frame = 0;
|
|
101
|
+
const step = (time: number) => {
|
|
102
|
+
const progress = Math.min(1, (time - startedAt) / SPEND_COUNT_UP_MS);
|
|
103
|
+
const eased = 1 - (1 - progress) ** 3;
|
|
104
|
+
const next = progress < 1 ? from + (value - from) * eased : value;
|
|
105
|
+
shownRef.current = next;
|
|
106
|
+
setShown(next);
|
|
107
|
+
if (progress < 1) {
|
|
108
|
+
frame = requestAnimationFrame(step);
|
|
109
|
+
} else {
|
|
110
|
+
setCounting(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
setCounting(true);
|
|
114
|
+
frame = requestAnimationFrame(step);
|
|
115
|
+
return () => cancelAnimationFrame(frame);
|
|
116
|
+
}, [value]);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<span
|
|
120
|
+
className={cn(
|
|
121
|
+
"transition-colors duration-700 motion-reduce:transition-none",
|
|
122
|
+
counting && "text-dashboard-accent-text",
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{shown === undefined ? "—" : formatCostSummary({ total: shown })}
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
52
130
|
const profileLinkClass =
|
|
53
131
|
"flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm font-semibold text-dashboard-text no-underline transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:bg-dashboard-fill-strong focus-visible:text-dashboard-text focus-visible:outline-none";
|
|
54
132
|
|
|
@@ -73,19 +151,10 @@ export function ProfileMenu({
|
|
|
73
151
|
(page) => page.navigation === "profile",
|
|
74
152
|
);
|
|
75
153
|
|
|
76
|
-
const avatar = (
|
|
77
|
-
<span
|
|
78
|
-
aria-hidden="true"
|
|
79
|
-
className="grid size-8 shrink-0 place-items-center rounded-full bg-dashboard-focus text-xs font-bold tracking-wide text-dashboard-text-inverse shadow-sm shadow-black/40"
|
|
80
|
-
>
|
|
81
|
-
{initials(identity.user.name, email)}
|
|
82
|
-
</span>
|
|
83
|
-
);
|
|
84
|
-
|
|
85
154
|
const identityBlock = (
|
|
86
155
|
<div className="border-b border-dashboard-border-strong px-2.5 py-2.5">
|
|
87
156
|
<div className="flex min-w-0 items-center gap-2.5">
|
|
88
|
-
{
|
|
157
|
+
<ProfileAvatar className="size-8" user={identity.user} />
|
|
89
158
|
<div className="min-w-0">
|
|
90
159
|
<p className="m-0 truncate text-sm font-semibold text-dashboard-text">
|
|
91
160
|
{name}
|
|
@@ -101,13 +170,13 @@ export function ProfileMenu({
|
|
|
101
170
|
<span>
|
|
102
171
|
7d{" "}
|
|
103
172
|
<span className="font-semibold text-dashboard-text">
|
|
104
|
-
{
|
|
173
|
+
<SpendAmount value={spend?.sevenDaysUsd} />
|
|
105
174
|
</span>
|
|
106
175
|
</span>
|
|
107
176
|
<span>
|
|
108
177
|
30d{" "}
|
|
109
178
|
<span className="font-semibold text-dashboard-text">
|
|
110
|
-
{
|
|
179
|
+
<SpendAmount value={spend?.thirtyDaysUsd} />
|
|
111
180
|
</span>
|
|
112
181
|
</span>
|
|
113
182
|
</div>
|
|
@@ -186,13 +255,13 @@ export function ProfileMenu({
|
|
|
186
255
|
<span className="whitespace-nowrap">
|
|
187
256
|
7d{" "}
|
|
188
257
|
<span className="font-semibold text-dashboard-text">
|
|
189
|
-
{
|
|
258
|
+
<SpendAmount value={spend?.sevenDaysUsd} />
|
|
190
259
|
</span>
|
|
191
260
|
</span>
|
|
192
261
|
<span className="whitespace-nowrap">
|
|
193
262
|
30d{" "}
|
|
194
263
|
<span className="font-semibold text-dashboard-text">
|
|
195
|
-
{
|
|
264
|
+
<SpendAmount value={spend?.thirtyDaysUsd} />
|
|
196
265
|
</span>
|
|
197
266
|
</span>
|
|
198
267
|
</div>
|
|
@@ -254,12 +323,7 @@ export function ProfileMenu({
|
|
|
254
323
|
aria-label={`Signed in as ${name}`}
|
|
255
324
|
className="flex min-w-0 items-center gap-2.5 px-1"
|
|
256
325
|
>
|
|
257
|
-
<
|
|
258
|
-
aria-hidden="true"
|
|
259
|
-
className="grid size-7 shrink-0 place-items-center rounded-full bg-dashboard-focus text-xs font-bold tracking-wide text-dashboard-text-inverse shadow-sm shadow-black/40"
|
|
260
|
-
>
|
|
261
|
-
{initials(identity.user.name, email)}
|
|
262
|
-
</span>
|
|
326
|
+
<ProfileAvatar user={identity.user} />
|
|
263
327
|
<div className="min-w-0">
|
|
264
328
|
<p className="m-0 truncate text-xs font-semibold text-dashboard-text">
|
|
265
329
|
{name}
|
|
@@ -304,7 +368,7 @@ export function ProfileMenu({
|
|
|
304
368
|
7d
|
|
305
369
|
</span>
|
|
306
370
|
<span className="text-xs font-semibold text-dashboard-text">
|
|
307
|
-
{
|
|
371
|
+
<SpendAmount value={spend?.sevenDaysUsd} />
|
|
308
372
|
</span>
|
|
309
373
|
</span>
|
|
310
374
|
<span
|
|
@@ -316,16 +380,11 @@ export function ProfileMenu({
|
|
|
316
380
|
30d
|
|
317
381
|
</span>
|
|
318
382
|
<span className="text-xs font-semibold text-dashboard-text">
|
|
319
|
-
{
|
|
383
|
+
<SpendAmount value={spend?.thirtyDaysUsd} />
|
|
320
384
|
</span>
|
|
321
385
|
</span>
|
|
322
386
|
</span>
|
|
323
|
-
<
|
|
324
|
-
aria-hidden="true"
|
|
325
|
-
className="grid size-7 place-items-center rounded-full bg-dashboard-focus text-xs font-bold tracking-wide text-dashboard-text-inverse shadow-sm shadow-black/40"
|
|
326
|
-
>
|
|
327
|
-
{initials(identity.user.name, email)}
|
|
328
|
-
</span>
|
|
387
|
+
<ProfileAvatar user={identity.user} />
|
|
329
388
|
<ChevronDown
|
|
330
389
|
aria-hidden="true"
|
|
331
390
|
className={cn(
|