@rozenite/network-activity-plugin 1.0.0-alpha.9 → 1.0.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 (111) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
  4. package/dist/assets/{App-CA1Fbh0I.js → App-C6wCDVkW.js} +8157 -2677
  5. package/dist/react-native.cjs +4 -1
  6. package/dist/react-native.js +4 -1
  7. package/dist/rozenite.json +1 -1
  8. package/dist/src/react-native/config.d.ts +20 -0
  9. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  10. package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
  11. package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
  12. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  13. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  14. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  15. package/dist/src/shared/client.d.ts +48 -4
  16. package/dist/src/shared/sse-events.d.ts +4 -1
  17. package/dist/src/ui/components/Button.d.ts +2 -2
  18. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  19. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  20. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  21. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  22. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  23. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  24. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
  25. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  26. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  27. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  28. package/dist/src/ui/components/RequestList.d.ts +9 -4
  29. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  30. package/dist/src/ui/components/Section.d.ts +8 -0
  31. package/dist/src/ui/components/Separator.d.ts +2 -1
  32. package/dist/src/ui/components/Tabs.d.ts +7 -0
  33. package/dist/src/ui/state/hooks.d.ts +4 -0
  34. package/dist/src/ui/state/model.d.ts +12 -7
  35. package/dist/src/ui/state/store.d.ts +27 -3
  36. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  37. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  38. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  39. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  40. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  41. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  42. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  43. package/dist/src/utils/cookieParser.d.ts +6 -0
  44. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  45. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  46. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  47. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  48. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  49. package/dist/src/utils/safeStringify.d.ts +1 -0
  50. package/dist/src/utils/typeChecks.d.ts +9 -0
  51. package/dist/useNetworkActivityDevTools.cjs +319 -24
  52. package/dist/useNetworkActivityDevTools.js +320 -25
  53. package/package.json +7 -4
  54. package/react-native.ts +6 -1
  55. package/src/react-native/config.ts +43 -0
  56. package/src/react-native/http/network-inspector.ts +170 -8
  57. package/src/react-native/http/overrides-registry.ts +32 -0
  58. package/src/react-native/http/xhr-interceptor.ts +19 -2
  59. package/src/react-native/sse/sse-inspector.ts +27 -5
  60. package/src/react-native/sse/sse-interceptor.ts +26 -8
  61. package/src/react-native/useNetworkActivityDevTools.ts +86 -8
  62. package/src/react-native/utils/getBlobName.ts +45 -0
  63. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  64. package/src/react-native/utils.ts +3 -3
  65. package/src/shared/client.ts +73 -4
  66. package/src/shared/sse-events.ts +4 -1
  67. package/src/ui/components/Button.tsx +1 -0
  68. package/src/ui/components/CodeBlock.tsx +19 -0
  69. package/src/ui/components/CodeEditor.tsx +26 -0
  70. package/src/ui/components/CookieCard.tsx +64 -0
  71. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  72. package/src/ui/components/DropdownMenu.tsx +206 -0
  73. package/src/ui/components/FilterBar.tsx +117 -0
  74. package/src/ui/components/Input.tsx +1 -1
  75. package/src/ui/components/JsonTree.tsx +10 -3
  76. package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
  77. package/src/ui/components/KeyValueGrid.tsx +51 -0
  78. package/src/ui/components/OverrideResponse.tsx +132 -0
  79. package/src/ui/components/RequestBody.tsx +86 -0
  80. package/src/ui/components/RequestList.tsx +65 -13
  81. package/src/ui/components/ScrollArea.tsx +1 -0
  82. package/src/ui/components/Section.tsx +46 -0
  83. package/src/ui/components/SidePanel.tsx +15 -5
  84. package/src/ui/globals.css +4 -0
  85. package/src/ui/hooks/useCopyToClipboard.ts +2 -2
  86. package/src/ui/state/hooks.ts +8 -0
  87. package/src/ui/state/model.ts +18 -7
  88. package/src/ui/state/store.ts +610 -500
  89. package/src/ui/tabs/CookiesTab.tsx +60 -263
  90. package/src/ui/tabs/HeadersTab.tsx +78 -89
  91. package/src/ui/tabs/RequestTab.tsx +58 -46
  92. package/src/ui/tabs/ResponseTab.tsx +98 -67
  93. package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
  94. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  95. package/src/ui/utils/escapeShellArg.ts +12 -0
  96. package/src/ui/utils/generateCurlCommand.ts +83 -0
  97. package/src/ui/utils/generateFetchCall.ts +64 -0
  98. package/src/ui/utils/generateMultipartBody.ts +19 -0
  99. package/src/ui/views/InspectorView.tsx +15 -3
  100. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  101. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  102. package/src/utils/cookieParser.ts +126 -0
  103. package/src/utils/getContentTypeMimeType.ts +17 -0
  104. package/src/utils/getHttpHeader.ts +17 -0
  105. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  106. package/src/utils/getStringSizeInBytes.ts +3 -0
  107. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  108. package/src/utils/safeStringify.ts +7 -0
  109. package/src/utils/typeChecks.ts +27 -0
  110. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  111. package/src/ui/utils/getHttpHeaderValue.ts +0 -14
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Extracts form data parts from a FormData object.
3
+ * Handles both the standard FormData API and the React Native FormData format.
4
+ *
5
+ * ```
6
+ * // node_modules/react-native/Libraries/Network/FormData.js
7
+ *
8
+ * class FormData {
9
+ * _parts: Array<FormDataNameValuePair>;
10
+ *
11
+ * constructor() {
12
+ * this._parts = [];
13
+ * }
14
+ *
15
+ * ...
16
+ * ```
17
+ */
18
+ export function getFormDataEntries(formData: any): [string, unknown][] {
19
+ if (!formData || typeof formData !== 'object') {
20
+ return [];
21
+ }
22
+
23
+ if (typeof formData.entries === 'function') {
24
+ return formData.entries();
25
+ }
26
+
27
+ if (Array.isArray(formData._parts)) {
28
+ return formData._parts;
29
+ }
30
+
31
+ return [];
32
+ }
@@ -1,4 +1,4 @@
1
- import { getHttpHeaderValue } from '../ui/utils/getHttpHeaderValue';
1
+ import { getContentTypeMime } from '../utils/getContentTypeMimeType';
2
2
 
3
3
  type UnionToIntersection<U> = (
4
4
  U extends unknown ? (k: U) => void : never
@@ -22,10 +22,10 @@ export const getContentType = (request: XMLHttpRequest): string => {
22
22
  const responseHeaders = request.responseHeaders;
23
23
  const responseType = request.responseType;
24
24
 
25
- const contentType = getHttpHeaderValue(responseHeaders || {}, 'content-type');
25
+ const contentType = getContentTypeMime(responseHeaders || {});
26
26
 
27
27
  if (contentType) {
28
- return contentType.split(';')[0].trim();
28
+ return contentType;
29
29
  }
30
30
 
31
31
  switch (responseType) {
@@ -2,17 +2,67 @@ import { RozeniteDevToolsClient } from '@rozenite/plugin-bridge';
2
2
  import { WebSocketEventMap } from './websocket-events';
3
3
  import { SSEEventMap } from './sse-events';
4
4
 
5
- export type HttpHeaders = Record<string, string>;
5
+ export type HttpHeaders = Record<string, string | string[]>;
6
+ export type XHRHeaders = NonNullable<XMLHttpRequest['responseHeaders']>;
7
+
6
8
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
7
9
 
8
10
  export type RequestId = string;
9
11
  export type Timestamp = number;
10
12
 
13
+ export type XHRPostData =
14
+ | string
15
+ | Blob
16
+ | FormData
17
+ | ArrayBuffer
18
+ | ArrayBufferView
19
+ | unknown
20
+ | null
21
+ | undefined;
22
+
23
+ export type RequestTextPostData = {
24
+ type: 'text';
25
+ value: string;
26
+ };
27
+
28
+ export type RequestBinaryPostData = {
29
+ type: 'binary';
30
+ value: {
31
+ size: number;
32
+ type?: string;
33
+ name?: string;
34
+ };
35
+ };
36
+
37
+ export type RequestFormDataPostData = {
38
+ type: 'form-data';
39
+ value: Record<string, RequestTextPostData | RequestBinaryPostData>;
40
+ };
41
+
42
+ export type RequestPostData =
43
+ | RequestTextPostData
44
+ | RequestFormDataPostData
45
+ | RequestBinaryPostData
46
+ | null
47
+ | undefined;
48
+
49
+ export type Cookie = {
50
+ name: string;
51
+ value: string;
52
+ domain?: string;
53
+ path?: string;
54
+ expires?: string;
55
+ maxAge?: string;
56
+ secure?: boolean;
57
+ httpOnly?: boolean;
58
+ sameSite?: string;
59
+ };
60
+
11
61
  export type Request = {
12
62
  url: string;
13
63
  method: HttpMethod;
14
64
  headers: HttpHeaders;
15
- postData?: string;
65
+ postData?: RequestPostData;
16
66
  };
17
67
 
18
68
  export type Response = {
@@ -21,7 +71,7 @@ export type Response = {
21
71
  statusText: string;
22
72
  headers: HttpHeaders;
23
73
  contentType: string;
24
- size: number;
74
+ size: number | null;
25
75
  responseTime: Timestamp;
26
76
  };
27
77
 
@@ -34,11 +84,26 @@ export type Initiator = {
34
84
 
35
85
  export type ResourceType = 'XHR' | 'Fetch' | 'Other';
36
86
 
87
+ export type RequestOverride = {
88
+ status?: number;
89
+ body?: string;
90
+ };
91
+
92
+ export type NetworkActivityClientUISettings = {
93
+ showUrlAsName?: boolean;
94
+ };
95
+
37
96
  export type NetworkActivityEventMap = {
38
97
  // Control events
39
98
  'network-enable': unknown;
40
99
  'network-disable': unknown;
41
100
 
101
+ // Client UI settings events
102
+ 'get-client-ui-settings': unknown;
103
+ 'client-ui-settings': {
104
+ settings?: NetworkActivityClientUISettings;
105
+ };
106
+
42
107
  // Network request events
43
108
  'request-sent': {
44
109
  requestId: RequestId;
@@ -59,7 +124,7 @@ export type NetworkActivityEventMap = {
59
124
  requestId: RequestId;
60
125
  timestamp: Timestamp;
61
126
  duration: number;
62
- size: number;
127
+ size: number | null;
63
128
  ttfb: number;
64
129
  };
65
130
 
@@ -79,6 +144,10 @@ export type NetworkActivityEventMap = {
79
144
  requestId: RequestId;
80
145
  body: string | null;
81
146
  };
147
+
148
+ 'set-overrides': {
149
+ overrides: [string, RequestOverride][];
150
+ };
82
151
  } & WebSocketEventMap &
83
152
  SSEEventMap;
84
153
 
@@ -14,7 +14,10 @@ export type SSEMessageEvent = {
14
14
  type: 'sse-message';
15
15
  requestId: SSERequestId;
16
16
  timestamp: number;
17
- data: string;
17
+ payload: {
18
+ type: string;
19
+ data: string;
20
+ };
18
21
  };
19
22
 
20
23
  export type SSEErrorEvent = {
@@ -21,6 +21,7 @@ const buttonVariants = cva(
21
21
  },
22
22
  size: {
23
23
  default: 'h-10 px-4 py-2',
24
+ xs: 'h-7 rounded-md px-2 text-xs',
24
25
  sm: 'h-9 rounded-md px-3',
25
26
  lg: 'h-11 rounded-md px-8',
26
27
  icon: 'h-10 w-10',
@@ -0,0 +1,19 @@
1
+ import { HTMLProps } from 'react';
2
+ import { cn } from '../utils/cn';
3
+
4
+ export type CodeBlockProps = HTMLProps<HTMLPreElement>;
5
+
6
+ const codeBlockClassNames =
7
+ 'text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded-md border border-gray-700 overflow-x-auto wrap-anywhere';
8
+
9
+ export const CodeBlock = ({
10
+ children,
11
+ className,
12
+ ...props
13
+ }: CodeBlockProps) => {
14
+ return (
15
+ <pre className={cn(codeBlockClassNames, className)} {...props}>
16
+ {children}
17
+ </pre>
18
+ );
19
+ };
@@ -0,0 +1,26 @@
1
+ import { forwardRef } from 'react';
2
+
3
+ export type CodeEditorProps = {
4
+ data: string | undefined;
5
+ onInput?: (event: React.FormEvent<HTMLPreElement>) => void;
6
+ };
7
+
8
+ export const CodeEditor = forwardRef<HTMLPreElement, CodeEditorProps>(
9
+ ({ data, onInput }, ref) => {
10
+ return (
11
+ <pre
12
+ ref={ref}
13
+ contentEditable
14
+ suppressContentEditableWarning
15
+ className={
16
+ 'w-full text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded-md border border-gray-700 overflow-x-auto wrap-anywhere ring-offset-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
17
+ }
18
+ onInput={onInput}
19
+ >
20
+ {data}
21
+ </pre>
22
+ );
23
+ }
24
+ );
25
+
26
+ CodeEditor.displayName = 'CodeEditor';
@@ -0,0 +1,64 @@
1
+ import { Badge } from './Badge';
2
+ import { Cookie } from '../../shared/client';
3
+ import { cn } from '../utils/cn';
4
+
5
+ type CookieCardProps = {
6
+ cookie: Cookie;
7
+ keyClassName?: string;
8
+ };
9
+
10
+ export const CookieCard = ({ cookie, keyClassName }: CookieCardProps) => (
11
+ <div className="bg-gray-800 border border-gray-700 rounded p-3">
12
+ <div className="flex items-center justify-between mb-2">
13
+ <span className={cn('text-sm font-medium', keyClassName)}>
14
+ {cookie.name}
15
+ </span>
16
+ <div className="flex items-center gap-2">
17
+ {cookie.secure && (
18
+ <Badge
19
+ variant="outline"
20
+ className="text-xs text-yellow-400 border-yellow-400"
21
+ >
22
+ Secure
23
+ </Badge>
24
+ )}
25
+ {cookie.httpOnly && (
26
+ <Badge
27
+ variant="outline"
28
+ className="text-xs text-purple-400 border-purple-400"
29
+ >
30
+ HttpOnly
31
+ </Badge>
32
+ )}
33
+ </div>
34
+ </div>
35
+ <div className="text-sm text-gray-300 mb-2 break-all">{cookie.value}</div>
36
+ <div className="grid grid-cols-2 gap-4 text-xs text-gray-400">
37
+ {cookie.domain && (
38
+ <div>
39
+ <span className="font-medium">Domain:</span> {cookie.domain}
40
+ </div>
41
+ )}
42
+ {cookie.path && (
43
+ <div>
44
+ <span className="font-medium">Path:</span> {cookie.path}
45
+ </div>
46
+ )}
47
+ {cookie.expires && (
48
+ <div>
49
+ <span className="font-medium">Expires:</span> {cookie.expires}
50
+ </div>
51
+ )}
52
+ {cookie.maxAge && (
53
+ <div>
54
+ <span className="font-medium">Max-Age:</span> {cookie.maxAge}
55
+ </div>
56
+ )}
57
+ {cookie.sameSite && (
58
+ <div>
59
+ <span className="font-medium">SameSite:</span> {cookie.sameSite}
60
+ </div>
61
+ )}
62
+ </div>
63
+ </div>
64
+ );
@@ -0,0 +1,95 @@
1
+ import { useCallback } from 'react';
2
+ import { Copy, Check, ChevronDown } from 'lucide-react';
3
+ import { Button } from './Button';
4
+ import { generateFetchCall } from '../utils/generateFetchCall';
5
+ import { generateCurlCommand } from '../utils/generateCurlCommand';
6
+ import { useCopyToClipboard } from '../hooks/useCopyToClipboard';
7
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from './DropdownMenu';
14
+ import { checkRequestBodyBinary } from '../utils/checkRequestBodyBinary';
15
+
16
+ type NetworkEntry = HttpNetworkEntry | SSENetworkEntry;
17
+
18
+ type CopyDropdownProps = {
19
+ selectedRequest: NetworkEntry;
20
+ };
21
+
22
+ type CopyOption = {
23
+ id: string;
24
+ label: string;
25
+ generate: (request: NetworkEntry) => string;
26
+ isEnabled: (request: NetworkEntry) => boolean;
27
+ };
28
+
29
+ const copyOptions: CopyOption[] = [
30
+ {
31
+ id: 'fetch',
32
+ label: 'fetch',
33
+ generate: generateFetchCall,
34
+ isEnabled: (request) => !checkRequestBodyBinary(request),
35
+ },
36
+ {
37
+ id: 'curl',
38
+ label: 'cURL',
39
+ generate: generateCurlCommand,
40
+ isEnabled: (request) =>
41
+ !checkRequestBodyBinary(request) || request.type === 'sse',
42
+ },
43
+ ];
44
+
45
+ export const CopyRequestDropdown = ({ selectedRequest }: CopyDropdownProps) => {
46
+ const { isCopied, copy } = useCopyToClipboard();
47
+
48
+ const handleCopy = useCallback(
49
+ async (option: CopyOption) => {
50
+ if (!selectedRequest) return;
51
+
52
+ try {
53
+ const content = await option.generate(selectedRequest);
54
+
55
+ await copy(content);
56
+ } catch (error) {
57
+ console.error(`Failed to copy ${option.label}:`, error);
58
+ }
59
+ },
60
+ [selectedRequest, copy]
61
+ );
62
+
63
+ const filteredCopyOptions = copyOptions.filter((option) =>
64
+ option.isEnabled(selectedRequest)
65
+ );
66
+
67
+ if (filteredCopyOptions.length === 0) {
68
+ return null;
69
+ }
70
+
71
+ return (
72
+ <DropdownMenu>
73
+ <DropdownMenuTrigger asChild>
74
+ <Button variant="ghost" size="xs" className="border border-gray-700">
75
+ {isCopied ? <Check size={16} /> : <Copy size={16} />}
76
+ Copy as ...
77
+ <ChevronDown size={12} className="ml-1" />
78
+ </Button>
79
+ </DropdownMenuTrigger>
80
+ <DropdownMenuContent align="start">
81
+ {filteredCopyOptions.map((option) => {
82
+ return (
83
+ <DropdownMenuItem
84
+ onClick={() => handleCopy(option)}
85
+ className="cursor-pointer"
86
+ key={option.id}
87
+ >
88
+ {option.label}
89
+ </DropdownMenuItem>
90
+ );
91
+ })}
92
+ </DropdownMenuContent>
93
+ </DropdownMenu>
94
+ );
95
+ };
@@ -0,0 +1,206 @@
1
+ import * as React from 'react';
2
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
+
4
+ import { cn } from '../utils/cn';
5
+
6
+ const DropdownMenu = DropdownMenuPrimitive.Root;
7
+
8
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
9
+
10
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
11
+
12
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
13
+
14
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
15
+
16
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
17
+
18
+ const DropdownMenuSubTrigger = React.forwardRef<
19
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
20
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
21
+ inset?: boolean;
22
+ }
23
+ >(({ className, inset, children, ...props }, ref) => (
24
+ <DropdownMenuPrimitive.SubTrigger
25
+ ref={ref}
26
+ className={cn(
27
+ 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-700 focus:text-gray-100 data-[state=open]:bg-gray-700 data-[state=open]:text-gray-100',
28
+ inset && 'pl-8',
29
+ className
30
+ )}
31
+ {...props}
32
+ >
33
+ {children}
34
+ </DropdownMenuPrimitive.SubTrigger>
35
+ ));
36
+ DropdownMenuSubTrigger.displayName =
37
+ DropdownMenuPrimitive.SubTrigger.displayName;
38
+
39
+ const DropdownMenuSubContent = React.forwardRef<
40
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
41
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
42
+ >(({ className, ...props }, ref) => (
43
+ <DropdownMenuPrimitive.SubContent
44
+ ref={ref}
45
+ className={cn(
46
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-600 bg-gray-800 p-1 text-gray-100 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
47
+ className
48
+ )}
49
+ {...props}
50
+ />
51
+ ));
52
+ DropdownMenuSubContent.displayName =
53
+ DropdownMenuPrimitive.SubContent.displayName;
54
+
55
+ const DropdownMenuContent = React.forwardRef<
56
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
57
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
58
+ >(({ className, sideOffset = 4, ...props }, ref) => (
59
+ <DropdownMenuPrimitive.Portal>
60
+ <DropdownMenuPrimitive.Content
61
+ ref={ref}
62
+ sideOffset={sideOffset}
63
+ className={cn(
64
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-600 bg-gray-800 p-1 text-gray-100 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ </DropdownMenuPrimitive.Portal>
70
+ ));
71
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
72
+
73
+ const DropdownMenuItem = React.forwardRef<
74
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
75
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
76
+ inset?: boolean;
77
+ }
78
+ >(({ className, inset, ...props }, ref) => (
79
+ <DropdownMenuPrimitive.Item
80
+ ref={ref}
81
+ className={cn(
82
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-gray-700 focus:text-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
83
+ inset && 'pl-8',
84
+ className
85
+ )}
86
+ {...props}
87
+ />
88
+ ));
89
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
90
+
91
+ const DropdownMenuCheckboxItem = React.forwardRef<
92
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
93
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
94
+ >(({ className, children, checked, ...props }, ref) => (
95
+ <DropdownMenuPrimitive.CheckboxItem
96
+ ref={ref}
97
+ className={cn(
98
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-700 focus:text-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
99
+ className
100
+ )}
101
+ checked={checked}
102
+ {...props}
103
+ >
104
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
105
+ <DropdownMenuPrimitive.ItemIndicator>
106
+ <svg
107
+ className="h-4 w-4"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ strokeWidth="2"
111
+ viewBox="0 0 24 24"
112
+ >
113
+ <polyline points="20,6 9,17 4,12" />
114
+ </svg>
115
+ </DropdownMenuPrimitive.ItemIndicator>
116
+ </span>
117
+ {children}
118
+ </DropdownMenuPrimitive.CheckboxItem>
119
+ ));
120
+ DropdownMenuCheckboxItem.displayName =
121
+ DropdownMenuPrimitive.CheckboxItem.displayName;
122
+
123
+ const DropdownMenuRadioItem = React.forwardRef<
124
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
125
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
126
+ >(({ className, children, ...props }, ref) => (
127
+ <DropdownMenuPrimitive.RadioItem
128
+ ref={ref}
129
+ className={cn(
130
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-700 focus:text-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
131
+ className
132
+ )}
133
+ {...props}
134
+ >
135
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
136
+ <DropdownMenuPrimitive.ItemIndicator>
137
+ <svg className="h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
138
+ <circle cx="4" cy="4" r="3" />
139
+ </svg>
140
+ </DropdownMenuPrimitive.ItemIndicator>
141
+ </span>
142
+ {children}
143
+ </DropdownMenuPrimitive.RadioItem>
144
+ ));
145
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
146
+
147
+ const DropdownMenuLabel = React.forwardRef<
148
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
149
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
150
+ inset?: boolean;
151
+ }
152
+ >(({ className, inset, ...props }, ref) => (
153
+ <DropdownMenuPrimitive.Label
154
+ ref={ref}
155
+ className={cn(
156
+ 'px-2 py-1.5 text-sm font-semibold text-gray-300',
157
+ inset && 'pl-8',
158
+ className
159
+ )}
160
+ {...props}
161
+ />
162
+ ));
163
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
164
+
165
+ const DropdownMenuSeparator = React.forwardRef<
166
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
167
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
168
+ >(({ className, ...props }, ref) => (
169
+ <DropdownMenuPrimitive.Separator
170
+ ref={ref}
171
+ className={cn('-mx-1 my-1 h-px bg-gray-600', className)}
172
+ {...props}
173
+ />
174
+ ));
175
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
176
+
177
+ const DropdownMenuShortcut = ({
178
+ className,
179
+ ...props
180
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
181
+ return (
182
+ <span
183
+ className={cn('ml-auto text-xs tracking-widest text-gray-400', className)}
184
+ {...props}
185
+ />
186
+ );
187
+ };
188
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
189
+
190
+ export {
191
+ DropdownMenu,
192
+ DropdownMenuTrigger,
193
+ DropdownMenuContent,
194
+ DropdownMenuItem,
195
+ DropdownMenuCheckboxItem,
196
+ DropdownMenuRadioItem,
197
+ DropdownMenuLabel,
198
+ DropdownMenuSeparator,
199
+ DropdownMenuShortcut,
200
+ DropdownMenuGroup,
201
+ DropdownMenuPortal,
202
+ DropdownMenuSub,
203
+ DropdownMenuSubContent,
204
+ DropdownMenuSubTrigger,
205
+ DropdownMenuRadioGroup,
206
+ };