@rozenite/network-activity-plugin 1.0.0-alpha.1 → 1.0.0-alpha.10

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 (134) hide show
  1. package/README.md +3 -5
  2. package/dist/{panel.html → App.html} +3 -3
  3. package/dist/assets/App-CA1Fbh0I.js +25364 -0
  4. package/dist/assets/App-DoHQsY5s.css +1276 -0
  5. package/dist/event-source.cjs +22 -0
  6. package/dist/event-source.js +23 -0
  7. package/dist/react-native.cjs +8 -1
  8. package/dist/react-native.d.ts +1 -5
  9. package/dist/react-native.js +6 -171
  10. package/dist/rozenite.config.d.ts +7 -0
  11. package/dist/rozenite.json +1 -1
  12. package/dist/src/react-native/http/network-inspector.d.ts +8 -0
  13. package/dist/src/react-native/http/network-requests-registry.d.ts +6 -0
  14. package/dist/src/react-native/http/xhr-interceptor.d.ts +38 -0
  15. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  16. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  17. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  18. package/dist/src/react-native/sse/types.d.ts +6 -0
  19. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -0
  20. package/dist/src/react-native/utils.d.ts +6 -0
  21. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  22. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  23. package/dist/src/shared/client.d.ts +68 -0
  24. package/dist/src/shared/sse-events.d.ts +35 -0
  25. package/dist/src/shared/websocket-events.d.ts +60 -0
  26. package/dist/src/ui/App.d.ts +1 -0
  27. package/dist/src/ui/components/Badge.d.ts +9 -0
  28. package/dist/src/ui/components/Button.d.ts +11 -0
  29. package/dist/src/ui/components/Input.d.ts +3 -0
  30. package/dist/src/ui/components/JsonTree.d.ts +5 -0
  31. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  32. package/dist/src/ui/components/RequestList.d.ts +25 -0
  33. package/dist/src/ui/components/ScrollArea.d.ts +4 -0
  34. package/dist/src/ui/components/Separator.d.ts +3 -0
  35. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  36. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  37. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  38. package/dist/src/ui/state/derived.d.ts +5 -0
  39. package/dist/src/ui/state/hooks.d.ts +17 -0
  40. package/dist/src/ui/state/model.d.ts +98 -0
  41. package/dist/src/ui/state/store.d.ts +24 -0
  42. package/dist/src/ui/tabs/CookiesTab.d.ts +5 -0
  43. package/dist/src/ui/tabs/HeadersTab.d.ts +5 -0
  44. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  45. package/dist/src/ui/tabs/RequestTab.d.ts +5 -0
  46. package/dist/src/ui/tabs/ResponseTab.d.ts +6 -0
  47. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  48. package/dist/src/ui/tabs/TimingTab.d.ts +5 -0
  49. package/dist/src/ui/types.d.ts +26 -0
  50. package/dist/src/ui/utils/assert.d.ts +1 -0
  51. package/dist/src/ui/utils/cn.d.ts +2 -0
  52. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  53. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +2 -0
  54. package/dist/src/ui/utils/getId.d.ts +1 -0
  55. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  56. package/dist/src/ui/views/InspectorView.d.ts +5 -0
  57. package/dist/src/ui/views/LoadingView.d.ts +1 -0
  58. package/dist/useNetworkActivityDevTools.cjs +759 -0
  59. package/dist/useNetworkActivityDevTools.js +757 -0
  60. package/package.json +31 -10
  61. package/postcss.config.js +6 -0
  62. package/project.json +12 -0
  63. package/react-native.ts +2 -1
  64. package/rozenite.config.ts +2 -2
  65. package/src/css-modules.d.ts +1 -1
  66. package/src/react-native/http/network-inspector.ts +226 -0
  67. package/src/react-native/http/network-requests-registry.ts +52 -0
  68. package/src/react-native/http/xhr-interceptor.ts +211 -0
  69. package/src/react-native/http/xml-request.d.ts +34 -0
  70. package/src/react-native/sse/event-source.ts +25 -0
  71. package/src/react-native/sse/sse-inspector.ts +117 -0
  72. package/src/react-native/sse/sse-interceptor.ts +162 -0
  73. package/src/react-native/sse/types.ts +9 -0
  74. package/src/react-native/useNetworkActivityDevTools.ts +73 -210
  75. package/src/react-native/utils.ts +43 -0
  76. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  77. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  78. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  79. package/src/shared/client.ts +86 -0
  80. package/src/shared/sse-events.ts +44 -0
  81. package/src/shared/websocket-events.ts +79 -0
  82. package/src/ui/App.tsx +19 -0
  83. package/src/ui/components/Badge.tsx +36 -0
  84. package/src/ui/components/Button.tsx +56 -0
  85. package/src/ui/components/Input.tsx +22 -0
  86. package/src/ui/components/JsonTree.tsx +50 -0
  87. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  88. package/src/ui/components/RequestList.tsx +295 -0
  89. package/src/ui/components/ScrollArea.tsx +48 -0
  90. package/src/ui/components/Separator.tsx +31 -0
  91. package/src/ui/components/SidePanel.tsx +323 -0
  92. package/src/ui/components/Tabs.tsx +55 -0
  93. package/src/ui/components/Toolbar.tsx +45 -0
  94. package/src/ui/globals.css +90 -0
  95. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  96. package/src/ui/state/derived.ts +112 -0
  97. package/src/ui/state/hooks.ts +44 -0
  98. package/src/ui/state/model.ts +129 -0
  99. package/src/ui/state/store.ts +559 -0
  100. package/src/ui/tabs/CookiesTab.tsx +279 -0
  101. package/src/ui/tabs/HeadersTab.tsx +110 -0
  102. package/src/ui/tabs/MessagesTab.tsx +276 -0
  103. package/src/ui/tabs/RequestTab.tsx +69 -0
  104. package/src/ui/tabs/ResponseTab.tsx +138 -0
  105. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  106. package/src/ui/tabs/TimingTab.tsx +60 -0
  107. package/src/ui/types.ts +34 -0
  108. package/src/ui/utils/assert.ts +5 -0
  109. package/src/ui/utils/cn.ts +6 -0
  110. package/src/ui/utils/copyToClipboard.ts +3 -0
  111. package/src/ui/utils/getHttpHeaderValue.ts +14 -0
  112. package/src/ui/utils/getId.ts +10 -0
  113. package/src/ui/utils/getStatusColor.ts +15 -0
  114. package/src/ui/views/InspectorView.tsx +53 -0
  115. package/src/ui/views/LoadingView.tsx +19 -0
  116. package/tailwind.config.ts +96 -0
  117. package/tsconfig.json +13 -6
  118. package/tsconfig.tsbuildinfo +1 -0
  119. package/vite.config.ts +13 -1
  120. package/dist/assets/panel-C5YgUUj5.js +0 -54
  121. package/dist/assets/panel-NCVczPb1.css +0 -1
  122. package/src/types/network.ts +0 -153
  123. package/src/ui/components.module.css +0 -158
  124. package/src/ui/components.tsx +0 -219
  125. package/src/ui/network-details.module.css +0 -57
  126. package/src/ui/network-details.tsx +0 -134
  127. package/src/ui/network-list.module.css +0 -122
  128. package/src/ui/network-list.tsx +0 -145
  129. package/src/ui/network-toolbar.module.css +0 -9
  130. package/src/ui/network-toolbar.tsx +0 -40
  131. package/src/ui/panel.module.css +0 -61
  132. package/src/ui/panel.tsx +0 -201
  133. package/src/ui/tanstack-query.tsx +0 -197
  134. package/src/ui/utils.ts +0 -89
@@ -0,0 +1,50 @@
1
+ import { JSONTree } from 'react-json-tree';
2
+ import { JsonTreeCopyableItem } from './JsonTreeCopyableItem';
3
+
4
+ export type JsonTreeProps = {
5
+ data: unknown;
6
+ shouldExpandNodeInitially?: () => boolean;
7
+ };
8
+
9
+ export const JsonTree = ({
10
+ data,
11
+ shouldExpandNodeInitially = () => true,
12
+ }: JsonTreeProps) => {
13
+ return (
14
+ <JSONTree
15
+ data={data}
16
+ theme={{
17
+ base00: 'transparent',
18
+ base01: '#374151', // bg-gray-700
19
+ base02: '#4b5563', // bg-gray-600
20
+ base03: '#6b7280', // text-gray-500
21
+ base04: '#9ca3af', // text-gray-400
22
+ base05: '#d1d5db', // text-gray-300
23
+ base06: '#e5e7eb', // text-gray-200
24
+ base07: '#f9fafb', // text-gray-100
25
+ base08: '#ef4444', // text-red-500
26
+ base09: '#f59e0b', // text-yellow-500
27
+ base0A: '#10b981', // text-green-500
28
+ base0B: '#3b82f6', // text-blue-500
29
+ base0C: '#06b6d4', // text-cyan-500
30
+ base0D: '#8b5cf6', // text-purple-500
31
+ base0E: '#ec4899', // text-pink-500
32
+ base0F: '#f97316', // text-orange-500
33
+ }}
34
+ invertTheme={false}
35
+ shouldExpandNodeInitially={shouldExpandNodeInitially}
36
+ // For objects and arrays
37
+ getItemString={(_type, data, itemType, itemString) => (
38
+ <JsonTreeCopyableItem getCopyableValue={() => JSON.stringify(data, null, 2)}>
39
+ <>{itemType} {itemString}</>
40
+ </JsonTreeCopyableItem>
41
+ )}
42
+ // For primitives
43
+ valueRenderer={(valueAsString, value) => (
44
+ <JsonTreeCopyableItem getCopyableValue={() => String(value)} className="ml-2">
45
+ {String(valueAsString)}
46
+ </JsonTreeCopyableItem>
47
+ )}
48
+ />
49
+ );
50
+ };
@@ -0,0 +1,33 @@
1
+ import { Check, Copy } from "lucide-react";
2
+ import { MouseEvent, PropsWithChildren } from "react";
3
+ import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
4
+ import { cn } from "../utils/cn";
5
+
6
+ type JsonTreeCopyableItemProps = PropsWithChildren<{
7
+ getCopyableValue: () => string;
8
+ className?: string;
9
+ }>;
10
+
11
+ export const JsonTreeCopyableItem = ({ children, getCopyableValue, className }: JsonTreeCopyableItemProps) => {
12
+ const { isCopied, copy } = useCopyToClipboard();
13
+
14
+ const handleCopy = (event: MouseEvent) => {
15
+ event.stopPropagation();
16
+
17
+ copy(getCopyableValue());
18
+ }
19
+
20
+ const Icon = isCopied ? Check : Copy;
21
+
22
+ return (
23
+ <span className={cn('inline-block group', className)}>
24
+ {children}
25
+ <div
26
+ className="inline-block cursor-pointer opacity-0 group-hover:opacity-100 text-gray-500 hover:text-gray-300 transition-all p-2 -m-2 ml-0 translate-y-0.75"
27
+ onClick={handleCopy}
28
+ >
29
+ <Icon className='h-4 w-4' />
30
+ </div>
31
+ </span>
32
+ );
33
+ }
@@ -0,0 +1,295 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ createColumnHelper,
4
+ flexRender,
5
+ getCoreRowModel,
6
+ getSortedRowModel,
7
+ SortingFn,
8
+ SortingState,
9
+ useReactTable,
10
+ } from '@tanstack/react-table';
11
+ import { ProcessedRequest } from '../state/model';
12
+ import { RequestId } from '../../shared/client';
13
+ import {
14
+ useNetworkActivityActions,
15
+ useProcessedRequests,
16
+ useSelectedRequestId,
17
+ } from '../state/hooks';
18
+ import { getStatusColor } from '../utils/getStatusColor';
19
+
20
+ type NetworkRequest = {
21
+ id: RequestId;
22
+ name: string;
23
+ status: string | number;
24
+ method: string;
25
+ domain: string;
26
+ path: string;
27
+ size: string;
28
+ time: string;
29
+ type: string;
30
+ startTime: string;
31
+ };
32
+
33
+ const formatSize = (bytes: number): string => {
34
+ if (bytes === 0) return '0 B';
35
+ const k = 1024;
36
+ const sizes = ['B', 'kB', 'MB', 'GB'];
37
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
38
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
39
+ };
40
+
41
+ const formatDuration = (duration: number): string => {
42
+ if (duration < 1000) return `${Math.round(duration)} ms`;
43
+ return `${(duration / 1000).toFixed(1)} s`;
44
+ };
45
+
46
+ const formatStartTime = (startTime: number): string => {
47
+ const date = new Date(startTime);
48
+ const timeString = date.toLocaleTimeString('en-US', {
49
+ hour12: false,
50
+ hour: '2-digit',
51
+ minute: '2-digit',
52
+ second: '2-digit',
53
+ });
54
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
55
+ return `${timeString}.${milliseconds}`;
56
+ };
57
+
58
+ const extractDomainAndPath = (
59
+ url: string
60
+ ): { domain: string; path: string } => {
61
+ try {
62
+ const { hostname, pathname, search, hash, port } = new URL(url);
63
+
64
+ return {
65
+ domain: `${hostname}${port ? `:${port}` : ''}`,
66
+ path: `${pathname}${search}${hash}`,
67
+ };
68
+ } catch {
69
+ return { domain: 'unknown', path: url };
70
+ }
71
+ };
72
+
73
+ const generateName = (url: string): string => {
74
+ try {
75
+ const urlObj = new URL(url);
76
+ const pathname = urlObj.pathname;
77
+ const filename = pathname.split('/').pop();
78
+ return filename || pathname || urlObj.hostname;
79
+ } catch {
80
+ return url;
81
+ }
82
+ };
83
+
84
+ const sortSize: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
85
+ const a = rowA.getValue(columnId) as string;
86
+ const b = rowB.getValue(columnId) as string;
87
+
88
+ // Extract numeric values from formatted strings like "1.2 kB", "500 B", etc.
89
+ const getNumericValue = (str: string) => {
90
+ const match = str.match(/^([\d.]+)\s*([KMGT]?B)$/);
91
+ if (!match) return 0;
92
+ const value = parseFloat(match[1]);
93
+ const unit = match[2];
94
+ const multipliers: Record<string, number> = {
95
+ B: 1,
96
+ kB: 1024,
97
+ MB: 1024 * 1024,
98
+ GB: 1024 * 1024 * 1024,
99
+ };
100
+ return value * (multipliers[unit] || 1);
101
+ };
102
+
103
+ return getNumericValue(a) - getNumericValue(b);
104
+ };
105
+
106
+ const sortTime: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
107
+ const a = rowA.getValue(columnId) as string;
108
+ const b = rowB.getValue(columnId) as string;
109
+
110
+ // Extract numeric values from formatted strings like "150 ms", "1.2 s", etc.
111
+ const getNumericValue = (str: string) => {
112
+ const match = str.match(/^([\d.]+)\s*(ms|s)$/);
113
+ if (!match) return 0;
114
+ const value = parseFloat(match[1]);
115
+ const unit = match[2];
116
+ return unit === 's' ? value * 1000 : value;
117
+ };
118
+
119
+ return getNumericValue(a) - getNumericValue(b);
120
+ };
121
+
122
+ const processNetworkRequests = (
123
+ processedRequests: ProcessedRequest[]
124
+ ): NetworkRequest[] => {
125
+ return processedRequests.map((request): NetworkRequest => {
126
+ const { domain, path } = extractDomainAndPath(request.name);
127
+ const duration = request.duration || 0;
128
+
129
+ return {
130
+ id: request.id,
131
+ name: generateName(request.name),
132
+ status: request.httpStatus || request.status,
133
+ method: request.method,
134
+ domain,
135
+ path,
136
+ size: formatSize(request.size || 0),
137
+ time: formatDuration(duration),
138
+ type: request.type,
139
+ startTime: formatStartTime(request.timestamp),
140
+ };
141
+ });
142
+ };
143
+
144
+ const columnHelper = createColumnHelper<NetworkRequest>();
145
+
146
+ const columns = [
147
+ columnHelper.accessor('startTime', {
148
+ header: 'Start Time',
149
+ cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
150
+ size: 120,
151
+ sortingFn: 'basic',
152
+ }),
153
+ columnHelper.accessor('name', {
154
+ header: 'Name',
155
+ cell: ({ getValue }) => (
156
+ <div className="flex-1 min-w-0 truncate">{getValue()}</div>
157
+ ),
158
+ sortingFn: 'alphanumeric',
159
+ }),
160
+ columnHelper.accessor('status', {
161
+ header: 'Status',
162
+ cell: ({ getValue }) => {
163
+ return (
164
+ <div className={`${getStatusColor(getValue())}`}>{getValue()}</div>
165
+ );
166
+ },
167
+ size: 64,
168
+ sortingFn: 'basic',
169
+ }),
170
+ columnHelper.accessor('method', {
171
+ header: 'Method',
172
+ cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
173
+ size: 64,
174
+ sortingFn: 'alphanumeric',
175
+ }),
176
+ columnHelper.accessor('domain', {
177
+ header: 'Domain',
178
+ cell: ({ getValue }) => (
179
+ <div className="text-gray-300 truncate">{getValue()}</div>
180
+ ),
181
+ size: 128,
182
+ sortingFn: 'alphanumeric',
183
+ }),
184
+ columnHelper.accessor('size', {
185
+ header: 'Size',
186
+ cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
187
+ size: 80,
188
+ sortingFn: sortSize,
189
+ }),
190
+ columnHelper.accessor('time', {
191
+ header: 'Time',
192
+ cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
193
+ size: 80,
194
+ sortingFn: sortTime,
195
+ }),
196
+ ];
197
+
198
+ export const RequestList = () => {
199
+ const actions = useNetworkActivityActions();
200
+ const processedRequests = useProcessedRequests();
201
+ const selectedRequestId = useSelectedRequestId();
202
+ const [sorting, setSorting] = useState<SortingState>([]);
203
+
204
+ const requests = useMemo(() => {
205
+ return processNetworkRequests(processedRequests);
206
+ }, [processedRequests]);
207
+
208
+ const table = useReactTable({
209
+ data: requests,
210
+ columns,
211
+ getCoreRowModel: getCoreRowModel(),
212
+ getSortedRowModel: getSortedRowModel(),
213
+ onSortingChange: setSorting,
214
+ state: {
215
+ sorting,
216
+ },
217
+ });
218
+
219
+ const onRequestSelect = (requestId: RequestId): void => {
220
+ actions.setSelectedRequest(requestId);
221
+ };
222
+
223
+ return (
224
+ <div className="flex-1 overflow-auto">
225
+ <table className="w-full">
226
+ <thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
227
+ {table.getHeaderGroups().map((headerGroup) => (
228
+ <tr key={headerGroup.id}>
229
+ {headerGroup.headers.map((header) => (
230
+ <th
231
+ key={header.id}
232
+ className={`text-left text-xs font-medium text-gray-400 px-2 py-2 ${
233
+ header.column.getCanSort()
234
+ ? 'cursor-pointer select-none hover:bg-gray-700'
235
+ : ''
236
+ }`}
237
+ style={{ width: header.getSize() }}
238
+ onClick={header.column.getToggleSortingHandler()}
239
+ >
240
+ <div className="flex items-center gap-1">
241
+ {header.isPlaceholder
242
+ ? null
243
+ : flexRender(
244
+ header.column.columnDef.header,
245
+ header.getContext()
246
+ )}
247
+ {header.column.getCanSort() && (
248
+ <span className="text-gray-500">
249
+ {{
250
+ asc: '↑',
251
+ desc: '↓',
252
+ }[header.column.getIsSorted() as string] ?? '↕'}
253
+ </span>
254
+ )}
255
+ </div>
256
+ </th>
257
+ ))}
258
+ </tr>
259
+ ))}
260
+ </thead>
261
+ <tbody>
262
+ {table.getRowModel().rows.map((row) => (
263
+ <tr
264
+ key={row.id}
265
+ className={`text-sm hover:bg-gray-800 cursor-pointer border-b border-gray-800 ${
266
+ selectedRequestId === row.original.id ? 'bg-blue-900/30' : ''
267
+ }`}
268
+ onClick={() => onRequestSelect(row.original.id)}
269
+ >
270
+ {row.getVisibleCells().map((cell) => (
271
+ <td
272
+ key={cell.id}
273
+ className="px-2 py-1"
274
+ style={{ width: cell.column.getSize() }}
275
+ >
276
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
277
+ </td>
278
+ ))}
279
+ </tr>
280
+ ))}
281
+ </tbody>
282
+ </table>
283
+ </div>
284
+ );
285
+ };
286
+
287
+ // Export helper functions for use in other components
288
+ export {
289
+ formatSize,
290
+ formatDuration,
291
+ formatStartTime,
292
+ extractDomainAndPath,
293
+ generateName,
294
+ processNetworkRequests,
295
+ };
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
5
+
6
+ import { cn } from '../utils/cn';
7
+
8
+ const ScrollArea = React.forwardRef<
9
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
11
+ >(({ className, children, ...props }, ref) => (
12
+ <ScrollAreaPrimitive.Root
13
+ ref={ref}
14
+ className={cn('relative overflow-hidden', className)}
15
+ {...props}
16
+ >
17
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
18
+ {children}
19
+ </ScrollAreaPrimitive.Viewport>
20
+ <ScrollBar />
21
+ <ScrollAreaPrimitive.Corner />
22
+ </ScrollAreaPrimitive.Root>
23
+ ));
24
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25
+
26
+ const ScrollBar = React.forwardRef<
27
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
28
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
29
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
30
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
31
+ ref={ref}
32
+ orientation={orientation}
33
+ className={cn(
34
+ 'flex touch-none select-none transition-colors',
35
+ orientation === 'vertical' &&
36
+ 'h-full w-2.5 border-l border-l-transparent p-[1px]',
37
+ orientation === 'horizontal' &&
38
+ 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
44
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
45
+ ));
46
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47
+
48
+ export { ScrollArea, ScrollBar };
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as SeparatorPrimitive from '@radix-ui/react-separator';
5
+
6
+ import { cn } from '../utils/cn';
7
+
8
+ const Separator = React.forwardRef<
9
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
+ >(
12
+ (
13
+ { className, orientation = 'horizontal', decorative = true, ...props },
14
+ ref
15
+ ) => (
16
+ <SeparatorPrimitive.Root
17
+ ref={ref}
18
+ decorative={decorative}
19
+ orientation={orientation}
20
+ className={cn(
21
+ 'shrink-0 bg-border',
22
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ );
29
+ Separator.displayName = SeparatorPrimitive.Root.displayName;
30
+
31
+ export { Separator };