@sentry/junior-dashboard 0.265.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.
Files changed (104) hide show
  1. package/dist/app.js +49 -12
  2. package/dist/client.js +94 -63
  3. package/dist/index.js +49 -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 +35 -53
  9. package/src/client/components/ActorAvatar.tsx +10 -22
  10. package/src/client/components/ConnectionBanner.tsx +4 -3
  11. package/src/client/components/CopyButton.tsx +3 -4
  12. package/src/client/components/DetailsPanel.tsx +12 -13
  13. package/src/client/components/Drawer.tsx +46 -23
  14. package/src/client/components/EmptyTelemetry.tsx +5 -3
  15. package/src/client/components/ImageAttachment.tsx +49 -63
  16. package/src/client/components/InlineError.tsx +8 -4
  17. package/src/client/components/Notice.tsx +19 -26
  18. package/src/client/components/Pagination.tsx +26 -26
  19. package/src/client/components/ParticipantAvatarStack.tsx +13 -10
  20. package/src/client/components/README.md +28 -3
  21. package/src/client/components/SearchInput.tsx +51 -41
  22. package/src/client/components/VersionDriftBanner.tsx +9 -7
  23. package/src/client/components/charts/CacheWrites.tsx +52 -43
  24. package/src/client/components/ui/alert.tsx +64 -0
  25. package/src/client/components/ui/avatar.tsx +53 -0
  26. package/src/client/components/ui/button-group.tsx +59 -0
  27. package/src/client/components/ui/button.tsx +2 -1
  28. package/src/client/components/ui/dialog.tsx +60 -0
  29. package/src/client/components/ui/drawer.tsx +140 -0
  30. package/src/client/components/ui/empty.tsx +61 -0
  31. package/src/client/components/ui/input-group.tsx +96 -0
  32. package/src/client/components/ui/pagination.tsx +60 -0
  33. package/src/client/components/ui/separator.tsx +22 -0
  34. package/src/client/components/ui/spinner.tsx +21 -0
  35. package/src/client/components/ui/table.tsx +22 -1
  36. package/src/client/components/ui/tooltip.tsx +1 -1
  37. package/src/client/components/ui/useSheet.ts +5 -1
  38. package/src/client/conversations/ComposerImages.tsx +66 -14
  39. package/src/client/conversations/ConversationChildAgents.tsx +5 -5
  40. package/src/client/conversations/ConversationComposer.tsx +84 -12
  41. package/src/client/conversations/ConversationDetails.tsx +3 -27
  42. package/src/client/conversations/ConversationEventDetails.tsx +26 -10
  43. package/src/client/conversations/ConversationHeader.tsx +5 -4
  44. package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
  45. package/src/client/conversations/ConversationPage.tsx +156 -2
  46. package/src/client/conversations/ConversationSidebar.tsx +5 -4
  47. package/src/client/conversations/ConversationTranscript.tsx +48 -16
  48. package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
  49. package/src/client/conversations/ConversationWatches.tsx +67 -59
  50. package/src/client/conversations/DetailsList.tsx +77 -29
  51. package/src/client/conversations/EventDetails.tsx +46 -19
  52. package/src/client/conversations/EventUsage.tsx +99 -74
  53. package/src/client/conversations/ForkConversationButton.tsx +79 -73
  54. package/src/client/conversations/PendingMailboxStack.tsx +312 -119
  55. package/src/client/conversations/README.md +30 -10
  56. package/src/client/conversations/ShareConversationButton.tsx +3 -4
  57. package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
  58. package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
  59. package/src/client/conversations/TranscriptAttachment.tsx +39 -4
  60. package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
  61. package/src/client/conversations/TranscriptEventView.tsx +35 -16
  62. package/src/client/conversations/TranscriptFailureView.tsx +10 -13
  63. package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
  64. package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
  65. package/src/client/conversations/TranscriptMessageView.tsx +7 -1
  66. package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
  67. package/src/client/conversations/TranscriptSummary.tsx +13 -7
  68. package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
  69. package/src/client/conversations/TranscriptView.tsx +7 -3
  70. package/src/client/conversations/UserInputRequestCard.tsx +244 -0
  71. package/src/client/conversations/conversationOutbox.ts +46 -0
  72. package/src/client/conversations/eventLog.ts +1 -0
  73. package/src/client/conversations/eventTime.ts +13 -1
  74. package/src/client/conversations/eventTranscript.ts +12 -5
  75. package/src/client/conversations/queries.ts +127 -26
  76. package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
  77. package/src/client/conversations/transcriptMentions.tsx +18 -5
  78. package/src/client/conversations/userInputRequest.ts +56 -0
  79. package/src/client/format.ts +10 -3
  80. package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
  81. package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
  82. package/src/client/pages/automations/AutomationEditor.tsx +182 -156
  83. package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
  84. package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
  85. package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
  86. package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
  87. package/src/client/pages/automations/AutomationOverview.tsx +238 -0
  88. package/src/client/pages/automations/AutomationPage.tsx +343 -0
  89. package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
  90. package/src/client/pages/automations/AutomationRow.tsx +11 -13
  91. package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
  92. package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
  93. package/src/client/pages/automations/AutomationStatus.tsx +164 -108
  94. package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
  95. package/src/client/pages/automations/AutomationsPage.tsx +10 -98
  96. package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
  97. package/src/client/pages/automations/README.md +56 -46
  98. package/src/client/types.ts +3 -0
  99. package/src/mock-reporting/fixtures.ts +44 -2
  100. package/src/mock-reporting/watches.ts +1 -1
  101. package/src/client/conversations/transcriptStyles.ts +0 -10
  102. package/src/client/pages/automations/AutomationDetails.tsx +0 -193
  103. package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
  104. package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
@@ -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/20 bg-rose-300/[0.08] text-rose-100 *:data-[slot=alert-description]:text-rose-100/70 *:[svg]:text-rose-300",
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 };
@@ -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 };
@@ -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 icon button surfaces. Links use it with `className`. */
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,60 @@
1
+ import type { ComponentProps } from "react";
2
+ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
3
+ import { cn } from "@dashboard/lib/utils";
4
+
5
+ /** Own the open state and modal focus rules for a centered dialog. */
6
+ function Dialog(props: DialogPrimitive.Root.Props) {
7
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
8
+ }
9
+
10
+ /** Close the dialog through Base UI dismissal and focus handling. */
11
+ function DialogClose(props: DialogPrimitive.Close.Props) {
12
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
13
+ }
14
+
15
+ /** Show a short task above the page. Small screens keep it near the top, clear of the keyboard. */
16
+ function DialogContent({ className, ...props }: DialogPrimitive.Popup.Props) {
17
+ return (
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
+ />
25
+ <DialogPrimitive.Popup
26
+ data-dashboard-modal=""
27
+ data-slot="dialog-content"
28
+ className={cn(
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",
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ </DialogPrimitive.Portal>
35
+ );
36
+ }
37
+
38
+ /** Name the dialog task. */
39
+ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
40
+ return (
41
+ <DialogPrimitive.Title
42
+ data-slot="dialog-title"
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)}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle };
@@ -0,0 +1,140 @@
1
+ import * as React from "react";
2
+ import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer";
3
+ import { cn } from "@dashboard/lib/utils";
4
+
5
+ type DrawerContextProps = {
6
+ modal: DrawerPrimitive.Root.Props["modal"];
7
+ swipeDirection: NonNullable<DrawerPrimitive.Root.Props["swipeDirection"]>;
8
+ };
9
+
10
+ const DrawerContext = React.createContext<DrawerContextProps | null>(null);
11
+
12
+ function useDrawer() {
13
+ const context = React.useContext(DrawerContext);
14
+
15
+ if (!context) {
16
+ throw new Error("useDrawer must be used within a Drawer.");
17
+ }
18
+
19
+ return context;
20
+ }
21
+
22
+ /**
23
+ * Own the open state, swipe dismissal, and nesting for a shadcn drawer.
24
+ * A drawer rendered inside another drawer stacks in front of it.
25
+ */
26
+ function Drawer({
27
+ modal = true,
28
+ swipeDirection = "down",
29
+ ...props
30
+ }: DrawerPrimitive.Root.Props) {
31
+ const contextValue = React.useMemo(
32
+ () => ({ modal, swipeDirection }),
33
+ [modal, swipeDirection],
34
+ );
35
+
36
+ return (
37
+ <DrawerContext.Provider value={contextValue}>
38
+ <DrawerPrimitive.Root
39
+ data-slot="drawer"
40
+ modal={modal}
41
+ swipeDirection={swipeDirection}
42
+ {...props}
43
+ />
44
+ </DrawerContext.Provider>
45
+ );
46
+ }
47
+
48
+ function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
49
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
50
+ }
51
+
52
+ /** Close the drawer through Base UI dismissal and focus handling. */
53
+ function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
54
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
55
+ }
56
+
57
+ function DrawerOverlay({
58
+ className,
59
+ ...props
60
+ }: DrawerPrimitive.Backdrop.Props) {
61
+ return (
62
+ <DrawerPrimitive.Backdrop
63
+ data-slot="drawer-overlay"
64
+ className={cn(
65
+ "fixed inset-0 z-50 min-h-dvh bg-dashboard-overlay opacity-[calc(1-var(--drawer-swipe-progress))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none motion-reduce:transition-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:opacity-0 data-swiping:duration-0 supports-[-webkit-touch-callout:none]:absolute",
66
+ className,
67
+ )}
68
+ {...props}
69
+ />
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Place the drawer on its swipe edge. While a nested drawer is open, this
75
+ * drawer scales back, peeks out behind it, and hides its content.
76
+ */
77
+ function DrawerContent({
78
+ className,
79
+ children,
80
+ ...props
81
+ }: DrawerPrimitive.Popup.Props) {
82
+ const { modal, swipeDirection } = useDrawer();
83
+ const swipeAxis =
84
+ swipeDirection === "down" || swipeDirection === "up" ? "y" : "x";
85
+
86
+ return (
87
+ <DrawerPortal>
88
+ {/* App drawers trap focus without a modal root, so keep the overlay.
89
+ Base UI skips it for a nested drawer. */}
90
+ <DrawerOverlay />
91
+ <DrawerPrimitive.Viewport
92
+ data-slot="drawer-viewport"
93
+ data-modal={modal}
94
+ className="pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto"
95
+ >
96
+ <DrawerPrimitive.Popup
97
+ data-slot="drawer-popup"
98
+ data-swipe-axis={swipeAxis}
99
+ className={cn(
100
+ // Base.
101
+ "group/drawer-popup pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] flex-col border-dashboard-border-strong bg-popover text-sm text-popover-foreground transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none motion-reduce:transition-none select-none [interpolate-size:allow-keywords] data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=down]:border-t data-[swipe-direction=left]:rounded-r-xl data-[swipe-direction=left]:border-r data-[swipe-direction=right]:rounded-l-xl data-[swipe-direction=right]:border-l data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=up]:border-b",
102
+ // Nested.
103
+ "data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95",
104
+ // Bleed.
105
+ "after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=x]:after:inset-y-0 data-[swipe-axis=x]:after:w-(--bleed) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:right-full data-[swipe-direction=right]:after:left-full data-[swipe-direction=up]:after:bottom-full",
106
+ // Sizing.
107
+ "[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:sm:[--drawer-content-width:24rem]",
108
+ // Stack.
109
+ "[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]",
110
+ // Transitions.
111
+ "data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]",
112
+ // Axis: y.
113
+ "data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)",
114
+ // Axis: x.
115
+ "data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row",
116
+ // Direction: down.
117
+ "data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]",
118
+ // Direction: up.
119
+ "data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]",
120
+ // Direction: left.
121
+ "data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]",
122
+ // Direction: right.
123
+ "data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]",
124
+ className,
125
+ )}
126
+ {...props}
127
+ >
128
+ <DrawerPrimitive.Content
129
+ data-slot="drawer-content"
130
+ className="flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] select-text motion-reduce:transition-none group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-swiping/drawer-popup:select-none"
131
+ >
132
+ {children}
133
+ </DrawerPrimitive.Content>
134
+ </DrawerPrimitive.Popup>
135
+ </DrawerPrimitive.Viewport>
136
+ </DrawerPortal>
137
+ );
138
+ }
139
+
140
+ export { Drawer, DrawerClose, DrawerContent };
@@ -0,0 +1,61 @@
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
+ /** Hold the icon that names the kind of empty state. */
19
+ function EmptyMedia({ className, ...props }: ComponentProps<"div">) {
20
+ return (
21
+ <div
22
+ aria-hidden="true"
23
+ data-slot="empty-media"
24
+ className={cn(
25
+ "grid size-8 shrink-0 place-items-center rounded-md bg-dashboard-fill-strong text-dashboard-text-subtle [&_svg:not([class*='size-'])]:size-4",
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ /** Name the empty state in a few words. */
34
+ function EmptyTitle({ className, ...props }: ComponentProps<"div">) {
35
+ return (
36
+ <div
37
+ data-slot="empty-title"
38
+ className={cn(
39
+ "font-display text-base font-medium leading-tight text-dashboard-text",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ /** Explain why the region is empty and what the user can do next. */
48
+ function EmptyDescription({ className, ...props }: ComponentProps<"div">) {
49
+ return (
50
+ <div
51
+ data-slot="empty-description"
52
+ className={cn(
53
+ "text-base leading-relaxed text-dashboard-text-muted",
54
+ className,
55
+ )}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ export { Empty, EmptyDescription, EmptyMedia, EmptyTitle };
@@ -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
+ };
@@ -0,0 +1,22 @@
1
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2
+ import { cn } from "@dashboard/lib/utils";
3
+
4
+ function Separator({
5
+ className,
6
+ orientation = "horizontal",
7
+ ...props
8
+ }: SeparatorPrimitive.Props) {
9
+ return (
10
+ <SeparatorPrimitive
11
+ data-slot="separator"
12
+ orientation={orientation}
13
+ className={cn(
14
+ "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ export { Separator };
@@ -0,0 +1,21 @@
1
+ import type { ComponentProps } from "react";
2
+ import { Loader2Icon } from "lucide-react";
3
+ import { cn } from "@dashboard/lib/utils";
4
+
5
+ /** Render a loading spinner. It stops spinning when the user prefers reduced motion. */
6
+ function Spinner({ className, ...props }: ComponentProps<"svg">) {
7
+ return (
8
+ <Loader2Icon
9
+ data-slot="spinner"
10
+ role="status"
11
+ aria-label="Loading"
12
+ className={cn(
13
+ "size-4 animate-spin motion-reduce:animate-none",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Spinner };