@sentry/junior-dashboard 0.265.0 → 0.266.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +2 -2
- package/dist/client.js +48 -48
- package/dist/index.js +2 -2
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +10 -10
- package/src/client/components/README.md +17 -1
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +66 -0
- package/src/client/components/ui/empty.tsx +32 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +44 -29
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/PendingMailboxStack.tsx +5 -10
- package/src/client/conversations/README.md +3 -0
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +34 -2
- package/src/client/conversations/TranscriptTurnContextView.tsx +45 -38
- package/src/client/conversations/eventLog.ts +1 -0
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/mock-reporting/watches.ts +1 -1
|
@@ -2,6 +2,12 @@ import type { Ref } from "react";
|
|
|
2
2
|
import { Search, X } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
import { cn } from "../styles";
|
|
5
|
+
import {
|
|
6
|
+
InputGroup,
|
|
7
|
+
InputGroupAddon,
|
|
8
|
+
InputGroupButton,
|
|
9
|
+
InputGroupInput,
|
|
10
|
+
} from "./ui/input-group";
|
|
5
11
|
|
|
6
12
|
type SearchInputSize = "compact" | "default";
|
|
7
13
|
|
|
@@ -15,23 +21,14 @@ export function SearchInput(props: {
|
|
|
15
21
|
size?: SearchInputSize;
|
|
16
22
|
value: string;
|
|
17
23
|
}) {
|
|
18
|
-
const size = props.size ?? "compact";
|
|
24
|
+
const size = sizeClass[props.size ?? "compact"];
|
|
19
25
|
return (
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
aria-hidden="true"
|
|
23
|
-
className={cn(
|
|
24
|
-
"pointer-events-none absolute top-1/2 -translate-y-1/2 text-dashboard-text-muted",
|
|
25
|
-
searchIconClass[size],
|
|
26
|
-
)}
|
|
27
|
-
size={size === "compact" ? 13 : 14}
|
|
28
|
-
strokeWidth={2.5}
|
|
29
|
-
/>
|
|
30
|
-
<input
|
|
26
|
+
<InputGroup className={cn(size.group, props.className)}>
|
|
27
|
+
<InputGroupInput
|
|
31
28
|
aria-label={props.label}
|
|
32
29
|
className={cn(
|
|
33
|
-
"
|
|
34
|
-
|
|
30
|
+
"[&::-webkit-search-cancel-button]:appearance-none",
|
|
31
|
+
size.input,
|
|
35
32
|
)}
|
|
36
33
|
onChange={(event) => props.onChange(event.currentTarget.value)}
|
|
37
34
|
placeholder={props.placeholder}
|
|
@@ -39,40 +36,53 @@ export function SearchInput(props: {
|
|
|
39
36
|
type="search"
|
|
40
37
|
value={props.value}
|
|
41
38
|
/>
|
|
42
|
-
{
|
|
43
|
-
<
|
|
39
|
+
<InputGroupAddon className={size.icon}>
|
|
40
|
+
<Search aria-hidden="true" size={size.iconSize} strokeWidth={2.5} />
|
|
41
|
+
</InputGroupAddon>
|
|
42
|
+
{/* The clear button keeps its space while hidden, so the field width does not change. */}
|
|
43
|
+
<InputGroupAddon align="inline-end" className={size.clear}>
|
|
44
|
+
<InputGroupButton
|
|
44
45
|
aria-label="Clear search"
|
|
45
|
-
className={
|
|
46
|
-
"absolute top-1/2 grid size-6 -translate-y-1/2 cursor-pointer place-items-center rounded-md text-dashboard-text-muted transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus",
|
|
47
|
-
clearButtonClass[size],
|
|
48
|
-
)}
|
|
46
|
+
className={props.value ? undefined : "invisible"}
|
|
49
47
|
onClick={(event) => {
|
|
50
48
|
props.onChange("");
|
|
51
|
-
// The button
|
|
52
|
-
event.currentTarget
|
|
49
|
+
// The button hides once the value is empty, so keep focus in the search box.
|
|
50
|
+
event.currentTarget
|
|
51
|
+
.closest("[data-slot=input-group]")
|
|
52
|
+
?.querySelector("input")
|
|
53
|
+
?.focus();
|
|
53
54
|
}}
|
|
54
|
-
type="button"
|
|
55
55
|
>
|
|
56
56
|
<X aria-hidden="true" size={14} strokeWidth={2.5} />
|
|
57
|
-
</
|
|
58
|
-
|
|
59
|
-
</
|
|
57
|
+
</InputGroupButton>
|
|
58
|
+
</InputGroupAddon>
|
|
59
|
+
</InputGroup>
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
compact:
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
"
|
|
63
|
+
// Match the icon, text, and clear button offsets of the bordered input.
|
|
64
|
+
const sizeClass: Record<
|
|
65
|
+
SearchInputSize,
|
|
66
|
+
{
|
|
67
|
+
clear: string;
|
|
68
|
+
group: string;
|
|
69
|
+
icon: string;
|
|
70
|
+
iconSize: number;
|
|
71
|
+
input: string;
|
|
72
|
+
}
|
|
73
|
+
> = {
|
|
74
|
+
compact: {
|
|
75
|
+
clear: "pr-1.25",
|
|
76
|
+
group: "h-9",
|
|
77
|
+
icon: "pl-2.25",
|
|
78
|
+
iconSize: 13,
|
|
79
|
+
input: "pr-1 pl-2.5",
|
|
80
|
+
},
|
|
81
|
+
default: {
|
|
82
|
+
clear: "pr-1.75",
|
|
83
|
+
group: "h-10",
|
|
84
|
+
icon: "pl-2.75",
|
|
85
|
+
iconSize: 14,
|
|
86
|
+
input: "pr-1 pl-2.75",
|
|
87
|
+
},
|
|
78
88
|
};
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { JUNIOR_VERSION } from "@sentry/junior/version";
|
|
2
2
|
|
|
3
|
+
import { Alert } from "./ui/alert";
|
|
4
|
+
import { Button } from "./ui/button";
|
|
5
|
+
|
|
3
6
|
/** Prompt the viewer to load the dashboard bundle that matches the server. */
|
|
4
7
|
export function VersionDriftBanner(props: { serverVersion?: string }) {
|
|
5
8
|
if (!dashboardVersionDrift(props.serverVersion)) return null;
|
|
6
9
|
|
|
7
10
|
return (
|
|
8
|
-
<
|
|
11
|
+
<Alert
|
|
9
12
|
aria-live="polite"
|
|
10
|
-
className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-t border-dashboard-accent/15 bg-dashboard-accent/[0.07] px-3 py-1.5 text-center font-sans text-xs text-dashboard-accent-text/80"
|
|
13
|
+
className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 rounded-none border-0 border-t border-dashboard-accent/15 bg-dashboard-accent/[0.07] px-3 py-1.5 text-center font-sans text-xs text-dashboard-accent-text/80"
|
|
11
14
|
role="status"
|
|
12
15
|
>
|
|
13
16
|
<span>The dashboard changed. Refresh to load the latest version.</span>
|
|
14
|
-
<
|
|
15
|
-
className="
|
|
17
|
+
<Button
|
|
18
|
+
className="h-auto border-0 bg-transparent p-0 text-xs font-semibold leading-normal text-dashboard-accent-text underline decoration-dashboard-accent/40 underline-offset-2 hover:bg-transparent hover:text-dashboard-text-solid"
|
|
16
19
|
onClick={() => window.location.reload()}
|
|
17
|
-
type="button"
|
|
18
20
|
>
|
|
19
21
|
Refresh
|
|
20
|
-
</
|
|
21
|
-
</
|
|
22
|
+
</Button>
|
|
23
|
+
</Alert>
|
|
22
24
|
);
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
CollapsibleContent,
|
|
10
10
|
CollapsibleTrigger,
|
|
11
11
|
} from "../ui/collapsible";
|
|
12
|
+
import {
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow,
|
|
19
|
+
} from "../ui/table";
|
|
12
20
|
import { dashboardDisclosureChevronClass } from "../../styles";
|
|
13
21
|
|
|
14
22
|
/** Identify the largest cache writes within the page's selected time window. */
|
|
@@ -40,50 +48,51 @@ export function CacheWrites(props: {
|
|
|
40
48
|
Top three by tokens written per {unit}, within the selected time
|
|
41
49
|
window. Large writes alone do not mean a cache problem.
|
|
42
50
|
</p>
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
>
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
51
|
+
<Table
|
|
52
|
+
aria-label="Largest cache writes"
|
|
53
|
+
className="min-w-80 table-fixed font-sans text-xs tabular-nums"
|
|
54
|
+
>
|
|
55
|
+
<TableHeader>
|
|
56
|
+
<TableRow>
|
|
57
|
+
<TableHead scope="col" className="px-0 py-2 font-normal">
|
|
58
|
+
{props.bucketUnit === "day" ? "Day" : "Starting at"}
|
|
59
|
+
</TableHead>
|
|
60
|
+
<TableHead
|
|
61
|
+
scope="col"
|
|
62
|
+
className="w-24 px-0 py-2 text-right font-normal"
|
|
63
|
+
>
|
|
64
|
+
Written
|
|
65
|
+
</TableHead>
|
|
66
|
+
<TableHead
|
|
67
|
+
scope="col"
|
|
68
|
+
className="w-24 px-0 py-2 text-right font-normal"
|
|
69
|
+
>
|
|
70
|
+
From cache
|
|
71
|
+
</TableHead>
|
|
72
|
+
</TableRow>
|
|
73
|
+
</TableHeader>
|
|
74
|
+
<TableBody>
|
|
75
|
+
{ranked.map((day) => (
|
|
76
|
+
<TableRow key={day.date}>
|
|
77
|
+
<TableHead
|
|
78
|
+
scope="row"
|
|
79
|
+
className="px-0 py-2 font-normal whitespace-normal"
|
|
66
80
|
>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</tr>
|
|
83
|
-
))}
|
|
84
|
-
</tbody>
|
|
85
|
-
</table>
|
|
86
|
-
</div>
|
|
81
|
+
{formatActivityTooltipDate(day.date)}
|
|
82
|
+
</TableHead>
|
|
83
|
+
<TableCell className="px-0 py-2 text-right whitespace-normal">
|
|
84
|
+
{formatCompactNumber(day.cacheCreationTokens)}
|
|
85
|
+
</TableCell>
|
|
86
|
+
<TableCell className="px-0 py-2 text-right whitespace-normal">
|
|
87
|
+
{formatCacheShare(
|
|
88
|
+
day.cachedInputTokens,
|
|
89
|
+
cacheInputTotal(day),
|
|
90
|
+
)}
|
|
91
|
+
</TableCell>
|
|
92
|
+
</TableRow>
|
|
93
|
+
))}
|
|
94
|
+
</TableBody>
|
|
95
|
+
</Table>
|
|
87
96
|
</CollapsibleContent>
|
|
88
97
|
</Collapsible>
|
|
89
98
|
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@dashboard/lib/utils";
|
|
4
|
+
|
|
5
|
+
const alertVariants = cva(
|
|
6
|
+
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default:
|
|
11
|
+
"border-dashboard-border-strong bg-dashboard-surface-raised text-dashboard-text",
|
|
12
|
+
destructive:
|
|
13
|
+
"border-rose-300/25 bg-dashboard-surface-raised text-rose-50 *:data-[slot=alert-description]:text-rose-100/70 *:[svg]:text-current",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
/** Show a message in a live region. Pass `role="status"` for polite updates. */
|
|
23
|
+
function Alert({
|
|
24
|
+
className,
|
|
25
|
+
variant,
|
|
26
|
+
...props
|
|
27
|
+
}: ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
data-slot="alert"
|
|
31
|
+
role="alert"
|
|
32
|
+
className={cn(alertVariants({ variant }), className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function AlertTitle({ className, ...props }: ComponentProps<"div">) {
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
data-slot="alert-title"
|
|
42
|
+
className={cn(
|
|
43
|
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-dashboard-text",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function AlertDescription({ className, ...props }: ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="alert-description"
|
|
55
|
+
className={cn(
|
|
56
|
+
"text-sm text-balance text-dashboard-text-muted md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-dashboard-text [&_p:not(:last-child)]:mb-4",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Alert, AlertTitle, AlertDescription };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
|
|
2
|
+
import { cn } from "@dashboard/lib/utils";
|
|
3
|
+
|
|
4
|
+
/** Render a round avatar frame. Use `size="sm"` in dense lists. */
|
|
5
|
+
function Avatar({
|
|
6
|
+
className,
|
|
7
|
+
size = "default",
|
|
8
|
+
...props
|
|
9
|
+
}: AvatarPrimitive.Root.Props & {
|
|
10
|
+
size?: "default" | "sm";
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<AvatarPrimitive.Root
|
|
14
|
+
data-slot="avatar"
|
|
15
|
+
data-size={size}
|
|
16
|
+
className={cn(
|
|
17
|
+
"group/avatar relative inline-grid size-7 shrink-0 place-items-center overflow-hidden rounded-full border-2 border-dashboard-bg bg-dashboard-focus shadow-sm select-none data-[size=sm]:size-6",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Show the avatar image after it loads. */
|
|
26
|
+
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
|
27
|
+
return (
|
|
28
|
+
<AvatarPrimitive.Image
|
|
29
|
+
data-slot="avatar-image"
|
|
30
|
+
className={cn("size-full object-cover", className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Show initials or a count while the image is missing, loading, or broken. */
|
|
37
|
+
function AvatarFallback({
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: AvatarPrimitive.Fallback.Props) {
|
|
41
|
+
return (
|
|
42
|
+
<AvatarPrimitive.Fallback
|
|
43
|
+
data-slot="avatar-fallback"
|
|
44
|
+
className={cn(
|
|
45
|
+
"grid size-full place-items-center font-sans text-xs leading-none font-semibold tracking-tight text-dashboard-text-inverse group-data-[size=sm]/avatar:text-2xs",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { Avatar, AvatarImage, AvatarFallback };
|
|
@@ -5,7 +5,7 @@ import { cn } from "@dashboard/lib/utils";
|
|
|
5
5
|
const commandButtonClass =
|
|
6
6
|
"inline-flex h-9 max-w-full items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium leading-none focus-visible:outline-2 focus-visible:outline-offset-2";
|
|
7
7
|
|
|
8
|
-
/** Style the dashboard command, primary, danger, and
|
|
8
|
+
/** Style the dashboard command, primary, danger, icon, and menu row button surfaces. Links use it with `className`. */
|
|
9
9
|
const buttonVariants = cva(
|
|
10
10
|
"cursor-pointer font-sans no-underline transition-colors data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
11
11
|
{
|
|
@@ -18,6 +18,7 @@ const buttonVariants = cva(
|
|
|
18
18
|
size: {
|
|
19
19
|
default: "",
|
|
20
20
|
icon: "grid size-9 place-items-center rounded-md border-0 bg-transparent p-0 text-dashboard-text-muted focus-visible:outline-2 focus-visible:outline-offset-1 data-disabled:hover:!bg-transparent data-disabled:hover:!text-dashboard-text-muted",
|
|
21
|
+
menu: "flex w-full items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text hover:bg-dashboard-fill-strong focus-visible:bg-dashboard-fill-strong focus-visible:outline-none data-disabled:hover:bg-transparent",
|
|
21
22
|
},
|
|
22
23
|
},
|
|
23
24
|
compoundVariants: [
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
|
2
|
+
import { cn } from "@dashboard/lib/utils";
|
|
3
|
+
|
|
4
|
+
/** Own the open state and modal focus rules for a dialog. */
|
|
5
|
+
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
|
6
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
|
10
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Close the dialog through Base UI dismissal and focus handling. */
|
|
14
|
+
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
|
15
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function DialogOverlay({
|
|
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. */
|
|
35
|
+
function DialogContent({ className, ...props }: DialogPrimitive.Popup.Props) {
|
|
36
|
+
return (
|
|
37
|
+
<DialogPortal>
|
|
38
|
+
<DialogOverlay />
|
|
39
|
+
<DialogPrimitive.Popup
|
|
40
|
+
data-slot="dialog-content"
|
|
41
|
+
data-dashboard-modal=""
|
|
42
|
+
className={cn(
|
|
43
|
+
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-dashboard-border-emphasis bg-dashboard-surface-raised p-4 text-sm text-dashboard-text shadow-lg outline-none transition-[opacity,scale] duration-180 motion-reduce:transition-none sm:max-w-sm data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:scale-95 data-starting-style:scale-95",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
</DialogPortal>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Give the dialog its accessible name. */
|
|
53
|
+
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
54
|
+
return (
|
|
55
|
+
<DialogPrimitive.Title
|
|
56
|
+
data-slot="dialog-title"
|
|
57
|
+
className={cn(
|
|
58
|
+
"m-0 text-base leading-none font-medium text-dashboard-text",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Dialog, DialogClose, DialogContent, DialogTitle };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "@dashboard/lib/utils";
|
|
3
|
+
|
|
4
|
+
/** Frame an empty state inside a page or list region. */
|
|
5
|
+
function Empty({ className, ...props }: ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="empty"
|
|
9
|
+
className={cn(
|
|
10
|
+
"min-w-0 rounded-lg border border-dashboard-border-strong bg-dashboard-bg px-4 py-3",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Explain why the region is empty and what the user can do next. */
|
|
19
|
+
function EmptyDescription({ className, ...props }: ComponentProps<"div">) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
data-slot="empty-description"
|
|
23
|
+
className={cn(
|
|
24
|
+
"text-base leading-relaxed text-dashboard-text-muted",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { Empty, EmptyDescription };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@dashboard/lib/utils";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@dashboard/components/ui/button";
|
|
6
|
+
import { Input } from "@dashboard/components/ui/input";
|
|
7
|
+
|
|
8
|
+
/** Frame one text input with its icons and inline buttons. */
|
|
9
|
+
function InputGroup({ className, ...props }: ComponentProps<"div">) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-slot="input-group"
|
|
13
|
+
role="group"
|
|
14
|
+
className={cn(
|
|
15
|
+
"group/input-group relative flex h-9 w-full min-w-0 items-center rounded-md border border-dashboard-border bg-dashboard-overlay-soft transition-colors outline-none hover:border-dashboard-border-emphasis has-[[data-slot=input-group-control]:focus-visible]:border-dashboard-focus has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-dashboard-focus/25",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const inputGroupAddonVariants = cva(
|
|
24
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-dashboard-text-muted select-none",
|
|
25
|
+
{
|
|
26
|
+
variants: {
|
|
27
|
+
align: {
|
|
28
|
+
"inline-start": "order-first pl-2.5",
|
|
29
|
+
"inline-end": "order-last pr-1.5",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
align: "inline-start",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/** Place an icon or button before or after the input. A click on the addon focuses the input. */
|
|
39
|
+
function InputGroupAddon({
|
|
40
|
+
className,
|
|
41
|
+
align = "inline-start",
|
|
42
|
+
...props
|
|
43
|
+
}: ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
role="group"
|
|
47
|
+
data-slot="input-group-addon"
|
|
48
|
+
data-align={align}
|
|
49
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
50
|
+
onClick={(e) => {
|
|
51
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
|
55
|
+
}}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Render a compact icon button inside an input group. */
|
|
62
|
+
function InputGroupButton({
|
|
63
|
+
className,
|
|
64
|
+
type = "button",
|
|
65
|
+
...props
|
|
66
|
+
}: Omit<ComponentProps<typeof Button>, "size" | "type"> & {
|
|
67
|
+
type?: "button" | "submit" | "reset";
|
|
68
|
+
}) {
|
|
69
|
+
return (
|
|
70
|
+
<Button
|
|
71
|
+
type={type}
|
|
72
|
+
size="icon"
|
|
73
|
+
className={cn("size-6", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Render the text input inside an input group. */
|
|
80
|
+
function InputGroupInput({
|
|
81
|
+
className,
|
|
82
|
+
...props
|
|
83
|
+
}: ComponentProps<typeof Input>) {
|
|
84
|
+
return (
|
|
85
|
+
<Input
|
|
86
|
+
data-slot="input-group-control"
|
|
87
|
+
className={cn(
|
|
88
|
+
"h-full min-w-0 flex-1 rounded-none border-0 bg-transparent px-2 focus:ring-0",
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
3
|
+
import { cn } from "@dashboard/lib/utils";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@dashboard/components/ui/button";
|
|
6
|
+
|
|
7
|
+
/** Mark a page control group as navigation. */
|
|
8
|
+
function Pagination({ className, ...props }: ComponentProps<"nav">) {
|
|
9
|
+
return (
|
|
10
|
+
<nav
|
|
11
|
+
role="navigation"
|
|
12
|
+
aria-label="Pagination"
|
|
13
|
+
data-slot="pagination"
|
|
14
|
+
className={cn("flex w-full justify-center", className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Hold the page controls as a list. */
|
|
21
|
+
function PaginationContent({ className, ...props }: ComponentProps<"ul">) {
|
|
22
|
+
return (
|
|
23
|
+
<ul
|
|
24
|
+
data-slot="pagination-content"
|
|
25
|
+
className={cn("flex items-center gap-2", className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Hold one page control. */
|
|
32
|
+
function PaginationItem({ ...props }: ComponentProps<"li">) {
|
|
33
|
+
return <li data-slot="pagination-item" {...props} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Go to the previous page. */
|
|
37
|
+
function PaginationPrevious(props: ComponentProps<typeof Button>) {
|
|
38
|
+
return (
|
|
39
|
+
<Button aria-label="Previous page" size="icon" {...props}>
|
|
40
|
+
<ChevronLeftIcon aria-hidden="true" size={16} />
|
|
41
|
+
</Button>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Go to the next page. */
|
|
46
|
+
function PaginationNext(props: ComponentProps<typeof Button>) {
|
|
47
|
+
return (
|
|
48
|
+
<Button aria-label="Next page" size="icon" {...props}>
|
|
49
|
+
<ChevronRightIcon aria-hidden="true" size={16} />
|
|
50
|
+
</Button>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
Pagination,
|
|
56
|
+
PaginationContent,
|
|
57
|
+
PaginationItem,
|
|
58
|
+
PaginationNext,
|
|
59
|
+
PaginationPrevious,
|
|
60
|
+
};
|