@rozenite/storage-plugin 1.4.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 (54) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +20 -0
  3. package/README.md +77 -0
  4. package/dist/assets/panel-C7usUotG.js +22 -0
  5. package/dist/assets/panel-DMhXYHH4.css +1 -0
  6. package/dist/panel.html +31 -0
  7. package/dist/react-native.cjs +1 -0
  8. package/dist/react-native.d.ts +4 -0
  9. package/dist/react-native.js +200 -0
  10. package/dist/rozenite.config.d.ts +7 -0
  11. package/dist/rozenite.json +1 -0
  12. package/dist/src/react-native/adapters/async-storage.d.ts +33 -0
  13. package/dist/src/react-native/adapters/index.d.ts +6 -0
  14. package/dist/src/react-native/adapters/mmkv.d.ts +13 -0
  15. package/dist/src/react-native/adapters/secure-storage.d.ts +18 -0
  16. package/dist/src/react-native/is-garbled.d.ts +1 -0
  17. package/dist/src/react-native/storage-view.d.ts +19 -0
  18. package/dist/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
  19. package/dist/src/shared/messaging.d.ts +29 -0
  20. package/dist/src/shared/types.d.ts +60 -0
  21. package/dist/src/ui/add-entry-dialog.d.ts +9 -0
  22. package/dist/src/ui/confirm-dialog.d.ts +11 -0
  23. package/dist/src/ui/edit-entry-dialog.d.ts +9 -0
  24. package/dist/src/ui/editable-table.d.ts +10 -0
  25. package/dist/src/ui/entry-detail-dialog.d.ts +8 -0
  26. package/dist/src/ui/panel.d.ts +1 -0
  27. package/dist/types.cjs +1 -0
  28. package/dist/types.js +11 -0
  29. package/dist/useRozeniteStoragePlugin.cjs +1 -0
  30. package/dist/useRozeniteStoragePlugin.js +212 -0
  31. package/package.json +59 -0
  32. package/postcss.config.js +6 -0
  33. package/react-native.ts +40 -0
  34. package/rozenite.config.ts +8 -0
  35. package/src/css-modules.d.ts +4 -0
  36. package/src/react-native/adapters/async-storage.ts +115 -0
  37. package/src/react-native/adapters/index.ts +19 -0
  38. package/src/react-native/adapters/mmkv.ts +187 -0
  39. package/src/react-native/adapters/secure-storage.ts +76 -0
  40. package/src/react-native/is-garbled.ts +17 -0
  41. package/src/react-native/storage-view.ts +245 -0
  42. package/src/react-native/useRozeniteStoragePlugin.ts +162 -0
  43. package/src/shared/messaging.ts +37 -0
  44. package/src/shared/types.ts +66 -0
  45. package/src/ui/add-entry-dialog.tsx +310 -0
  46. package/src/ui/confirm-dialog.tsx +100 -0
  47. package/src/ui/edit-entry-dialog.tsx +280 -0
  48. package/src/ui/editable-table.tsx +298 -0
  49. package/src/ui/entry-detail-dialog.tsx +220 -0
  50. package/src/ui/globals.css +123 -0
  51. package/src/ui/panel.tsx +442 -0
  52. package/tailwind.config.ts +94 -0
  53. package/tsconfig.json +36 -0
  54. package/vite.config.ts +20 -0
@@ -0,0 +1,298 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ createColumnHelper,
4
+ flexRender,
5
+ getCoreRowModel,
6
+ getFilteredRowModel,
7
+ getSortedRowModel,
8
+ type ColumnDef,
9
+ type SortingState,
10
+ useReactTable,
11
+ } from '@tanstack/react-table';
12
+ import { Edit3, Loader2, Trash2 } from 'lucide-react';
13
+ import type {
14
+ StorageEntry,
15
+ StorageEntryType,
16
+ StorageEntryValue,
17
+ } from '../shared/types';
18
+ import { ConfirmDialog } from './confirm-dialog';
19
+ import { EditEntryDialog } from './edit-entry-dialog';
20
+
21
+ export type EditableTableProps = {
22
+ data: StorageEntry[];
23
+ supportedTypes: StorageEntryType[];
24
+ onValueChange?: (key: string, newValue: StorageEntryValue) => void;
25
+ onDeleteEntry?: (key: string) => void;
26
+ onRowClick?: (entry: StorageEntry) => void;
27
+ loading?: boolean;
28
+ };
29
+
30
+ const columnHelper = createColumnHelper<StorageEntry>();
31
+
32
+ export const EditableTable = ({
33
+ data,
34
+ supportedTypes,
35
+ onValueChange,
36
+ onDeleteEntry,
37
+ onRowClick,
38
+ loading = false,
39
+ }: EditableTableProps) => {
40
+ const [editingEntry, setEditingEntry] = useState<StorageEntry | null>(null);
41
+ const [showEditDialog, setShowEditDialog] = useState(false);
42
+ const [sorting, setSorting] = useState<SortingState>([]);
43
+ const [deleteConfirm, setDeleteConfirm] = useState<{
44
+ isOpen: boolean;
45
+ entryKey: string;
46
+ }>({ isOpen: false, entryKey: '' });
47
+
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ const columns = useMemo<ColumnDef<StorageEntry, any>[]>(
50
+ () => [
51
+ columnHelper.accessor('key', {
52
+ header: 'Key',
53
+ enableSorting: true,
54
+ cell: ({ getValue }) => (
55
+ <div className="text-gray-300 font-mono text-sm">{getValue()}</div>
56
+ ),
57
+ }),
58
+ columnHelper.accessor('type', {
59
+ header: 'Type',
60
+ enableSorting: true,
61
+ cell: ({ getValue }) => {
62
+ const type = getValue() as StorageEntryType;
63
+ return (
64
+ <div className="flex items-center">
65
+ <span
66
+ className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
67
+ type
68
+ )}`}
69
+ >
70
+ {type}
71
+ </span>
72
+ </div>
73
+ );
74
+ },
75
+ }),
76
+ columnHelper.accessor('value', {
77
+ header: 'Value',
78
+ cell: ({ row }) => {
79
+ const entry = row.original;
80
+ return (
81
+ <div className="flex items-center justify-between group">
82
+ <div className="flex-1">{formatValue(entry)}</div>
83
+ <button
84
+ onClick={() => handleEdit(entry)}
85
+ className="opacity-0 group-hover:opacity-100 ml-2 p-1 text-gray-400 hover:text-blue-400 hover:bg-gray-800 rounded transition-all"
86
+ title="Edit value"
87
+ aria-label={`Edit value for ${entry.key}`}
88
+ >
89
+ <Edit3 className="h-3 w-3" />
90
+ </button>
91
+ </div>
92
+ );
93
+ },
94
+ }),
95
+ columnHelper.display({
96
+ id: 'actions',
97
+ header: 'Actions',
98
+ cell: ({ row }) => (
99
+ <div className="flex items-center gap-2">
100
+ <button
101
+ onClick={() => handleDelete(row.original.key)}
102
+ disabled={!onDeleteEntry}
103
+ className="p-1 text-gray-400 hover:text-red-400 hover:bg-gray-800 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
104
+ title="Delete entry"
105
+ aria-label={`Delete entry ${row.original.key}`}
106
+ >
107
+ <Trash2 className="h-4 w-4" />
108
+ </button>
109
+ </div>
110
+ ),
111
+ }),
112
+ ],
113
+ [onDeleteEntry]
114
+ );
115
+
116
+ const table = useReactTable({
117
+ data,
118
+ columns,
119
+ state: { sorting },
120
+ onSortingChange: setSorting,
121
+ getCoreRowModel: getCoreRowModel(),
122
+ getFilteredRowModel: getFilteredRowModel(),
123
+ getSortedRowModel: getSortedRowModel(),
124
+ });
125
+
126
+ const handleEdit = (entry: StorageEntry) => {
127
+ setEditingEntry(entry);
128
+ setShowEditDialog(true);
129
+ };
130
+
131
+ const handleEditEntry = (key: string, newValue: StorageEntryValue) => {
132
+ if (onValueChange) {
133
+ onValueChange(key, newValue);
134
+ }
135
+
136
+ setEditingEntry(null);
137
+ setShowEditDialog(false);
138
+ };
139
+
140
+ const handleCloseEditDialog = () => {
141
+ setEditingEntry(null);
142
+ setShowEditDialog(false);
143
+ };
144
+
145
+ const handleDelete = (key: string) => {
146
+ if (onDeleteEntry) {
147
+ setDeleteConfirm({ isOpen: true, entryKey: key });
148
+ }
149
+ };
150
+
151
+ const confirmDelete = () => {
152
+ if (onDeleteEntry && deleteConfirm.entryKey) {
153
+ onDeleteEntry(deleteConfirm.entryKey);
154
+ }
155
+
156
+ setDeleteConfirm({ isOpen: false, entryKey: '' });
157
+ };
158
+
159
+ if (loading) {
160
+ return (
161
+ <div className="flex flex-col items-center justify-center h-full w-full">
162
+ <Loader2 className="h-8 w-8 text-blue-400 animate-spin mb-4" />
163
+ <p className="text-gray-400">Loading entries...</p>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ return (
169
+ <>
170
+ <table className="w-full self-start">
171
+ <thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
172
+ {table.getHeaderGroups().map((headerGroup) => (
173
+ <tr key={headerGroup.id}>
174
+ {headerGroup.headers.map((header) => (
175
+ <th
176
+ key={header.id}
177
+ className={`text-left text-xs font-medium text-gray-400 px-3 py-2 ${
178
+ header.column.getCanSort()
179
+ ? 'cursor-pointer select-none hover:bg-gray-700'
180
+ : ''
181
+ }`}
182
+ onClick={header.column.getToggleSortingHandler()}
183
+ >
184
+ <div className="flex items-center gap-1">
185
+ {header.isPlaceholder
186
+ ? null
187
+ : flexRender(
188
+ header.column.columnDef.header,
189
+ header.getContext()
190
+ )}
191
+ {header.column.getCanSort() && (
192
+ <span className="text-gray-500">
193
+ {{
194
+ asc: '↑',
195
+ desc: '↓',
196
+ }[header.column.getIsSorted() as string] ?? '↕'}
197
+ </span>
198
+ )}
199
+ </div>
200
+ </th>
201
+ ))}
202
+ </tr>
203
+ ))}
204
+ </thead>
205
+ <tbody>
206
+ {table.getRowModel().rows.map((row) => (
207
+ <tr
208
+ key={row.id}
209
+ className={`text-sm hover:bg-gray-800 border-b border-gray-800 ${
210
+ onRowClick ? 'cursor-pointer' : ''
211
+ }`}
212
+ onClick={(event) => {
213
+ const target = event.target as HTMLElement;
214
+ if (
215
+ target.tagName === 'BUTTON' ||
216
+ target.closest('button') ||
217
+ target.tagName === 'INPUT' ||
218
+ target.closest('input')
219
+ ) {
220
+ return;
221
+ }
222
+
223
+ if (onRowClick) {
224
+ onRowClick(row.original);
225
+ }
226
+ }}
227
+ >
228
+ {row.getVisibleCells().map((cell) => (
229
+ <td key={cell.id} className="px-3 py-2">
230
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
231
+ </td>
232
+ ))}
233
+ </tr>
234
+ ))}
235
+ </tbody>
236
+ </table>
237
+
238
+ <EditEntryDialog
239
+ isOpen={showEditDialog}
240
+ onClose={handleCloseEditDialog}
241
+ onEditEntry={handleEditEntry}
242
+ entry={editingEntry}
243
+ supportedTypes={supportedTypes}
244
+ />
245
+
246
+ <ConfirmDialog
247
+ isOpen={deleteConfirm.isOpen}
248
+ onClose={() => setDeleteConfirm({ isOpen: false, entryKey: '' })}
249
+ onConfirm={confirmDelete}
250
+ title="Delete Entry"
251
+ message={`Are you sure you want to delete the entry "${deleteConfirm.entryKey}"?`}
252
+ type="confirm"
253
+ confirmText="Delete"
254
+ />
255
+ </>
256
+ );
257
+ };
258
+
259
+ const getTypeColorClass = (type: StorageEntryType) => {
260
+ if (type === 'string') {
261
+ return 'bg-green-600';
262
+ }
263
+
264
+ if (type === 'number') {
265
+ return 'bg-blue-600';
266
+ }
267
+
268
+ if (type === 'boolean') {
269
+ return 'bg-yellow-600';
270
+ }
271
+
272
+ return 'bg-purple-600';
273
+ };
274
+
275
+ const formatValue = (entry: StorageEntry) => {
276
+ if (entry.type === 'string') {
277
+ return <span className="text-green-300 font-mono">"{entry.value}"</span>;
278
+ }
279
+
280
+ if (entry.type === 'number') {
281
+ return <span className="text-blue-300 font-mono">{entry.value}</span>;
282
+ }
283
+
284
+ if (entry.type === 'boolean') {
285
+ return (
286
+ <span className={`font-mono ${entry.value ? 'text-green-400' : 'text-red-400'}`}>
287
+ {entry.value ? 'true' : 'false'}
288
+ </span>
289
+ );
290
+ }
291
+
292
+ const displayValue =
293
+ entry.value.length > 5
294
+ ? `[${entry.value.slice(0, 5).join(', ')}, ...${entry.value.length - 5} more]`
295
+ : `[${entry.value.join(', ')}]`;
296
+
297
+ return <span className="text-purple-300 font-mono">{displayValue}</span>;
298
+ };
@@ -0,0 +1,220 @@
1
+ import { X, Info, Edit3 } from 'lucide-react';
2
+ import { JSONTree } from 'react-json-tree';
3
+ import { StorageEntry } from '../shared/types';
4
+ import { useMemo } from 'react';
5
+
6
+ export type EntryDetailDialogProps = {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onEdit?: (entry: StorageEntry) => void;
10
+ entry: StorageEntry | null;
11
+ };
12
+
13
+ const jsonTreeTheme = {
14
+ base00: 'transparent',
15
+ base01: '#374151', // bg-gray-700
16
+ base02: '#4b5563', // bg-gray-600
17
+ base03: '#6b7280', // text-gray-500
18
+ base04: '#9ca3af', // text-gray-400
19
+ base05: '#d1d5db', // text-gray-300
20
+ base06: '#e5e7eb', // text-gray-200
21
+ base07: '#f9fafb', // text-gray-100
22
+ base08: '#ef4444', // text-red-500
23
+ base09: '#f59e0b', // text-yellow-500
24
+ base0A: '#10b981', // text-green-500
25
+ base0B: '#3b82f6', // text-blue-500
26
+ base0C: '#06b6d4', // text-cyan-500
27
+ base0D: '#8b5cf6', // text-purple-500
28
+ base0E: '#ec4899', // text-pink-500
29
+ base0F: '#f97316', // text-orange-500
30
+ };
31
+
32
+ const jsonSafeParse = (value: string): Record<string, unknown> | unknown[] | null => {
33
+ try {
34
+ const parsed = JSON.parse(value) as unknown;
35
+
36
+ if (Array.isArray(parsed)) {
37
+ return parsed;
38
+ }
39
+
40
+ if (parsed && typeof parsed === 'object') {
41
+ return parsed as Record<string, unknown>;
42
+ }
43
+
44
+ return null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ };
49
+
50
+ const getTypeColorClass = (type: string) => {
51
+ switch (type) {
52
+ case 'string':
53
+ return 'bg-green-600';
54
+ case 'number':
55
+ return 'bg-blue-600';
56
+ case 'boolean':
57
+ return 'bg-yellow-600';
58
+ case 'buffer':
59
+ return 'bg-purple-600';
60
+ default:
61
+ return 'bg-gray-600';
62
+ }
63
+ };
64
+
65
+ const formatValue = (entry: StorageEntry) => {
66
+ switch (entry.type) {
67
+ case 'string':
68
+ return (
69
+ <span className="text-green-300 font-mono break-all">
70
+ "{entry.value as string}"
71
+ </span>
72
+ );
73
+ case 'number':
74
+ return (
75
+ <span className="text-blue-300 font-mono">{entry.value as number}</span>
76
+ );
77
+ case 'boolean':
78
+ return (
79
+ <span
80
+ className={`font-mono ${
81
+ entry.value ? 'text-green-400' : 'text-red-400'
82
+ }`}
83
+ >
84
+ {entry.value ? 'true' : 'false'}
85
+ </span>
86
+ );
87
+ case 'buffer': {
88
+ const bufferArray = entry.value as number[];
89
+ return (
90
+ <span className="text-purple-300 font-mono">
91
+ [{bufferArray.join(', ')}]
92
+ </span>
93
+ );
94
+ }
95
+ default:
96
+ return <span className="text-gray-400">Unknown</span>;
97
+ }
98
+ };
99
+
100
+ export const EntryDetailDialog = ({
101
+ isOpen,
102
+ onClose,
103
+ onEdit,
104
+ entry,
105
+ }: EntryDetailDialogProps) => {
106
+ const isStringValue = entry?.type === 'string';
107
+ const stringValue = isStringValue ? (entry.value as string) : '';
108
+ const jsonValue = useMemo(
109
+ () => (isStringValue ? jsonSafeParse(stringValue) : null),
110
+ [isStringValue, stringValue]
111
+ );
112
+
113
+ const handleKeyDown = (e: React.KeyboardEvent) => {
114
+ if (e.key === 'Escape') {
115
+ onClose();
116
+ }
117
+ };
118
+
119
+ if (!isOpen || !entry) return null;
120
+
121
+ return (
122
+ <div
123
+ className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
124
+ onClick={onClose}
125
+ >
126
+ <div
127
+ className="bg-gray-800 rounded-lg p-6 w-[90vw] max-w-2xl max-h-[90vh] flex flex-col"
128
+ onClick={(e) => e.stopPropagation()}
129
+ onKeyDown={handleKeyDown}
130
+ >
131
+ <div className="flex items-center justify-between mb-4">
132
+ <div className="flex items-center gap-2">
133
+ <Info className="h-5 w-5 text-blue-400" />
134
+ <h2 className="text-lg font-semibold text-gray-100">
135
+ Entry Details
136
+ </h2>
137
+ </div>
138
+ <button
139
+ onClick={onClose}
140
+ className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
141
+ title="Close dialog"
142
+ >
143
+ <X className="h-4 w-4" />
144
+ </button>
145
+ </div>
146
+
147
+ <div className="space-y-4 flex-1 overflow-auto">
148
+ {/* Key Display */}
149
+ <div>
150
+ <label className="block text-sm font-medium text-gray-200 mb-1">
151
+ Key
152
+ </label>
153
+ <div className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 font-mono break-all">
154
+ {entry.key}
155
+ </div>
156
+ </div>
157
+
158
+ {/* Type Display */}
159
+ <div>
160
+ <label className="block text-sm font-medium text-gray-200 mb-1">
161
+ Type
162
+ </label>
163
+ <div className="flex items-center">
164
+ <span
165
+ className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
166
+ entry.type
167
+ )}`}
168
+ >
169
+ {entry.type}
170
+ </span>
171
+ </div>
172
+ </div>
173
+
174
+ {/* Value Display */}
175
+ <div className="flex-1 min-h-0 flex flex-col">
176
+ <label className="block text-sm font-medium text-gray-200 mb-1">
177
+ Value
178
+ </label>
179
+ <div className="max-h-96 overflow-auto bg-gray-700 border border-gray-600 rounded p-3">
180
+ {jsonValue ? (
181
+ <JSONTree
182
+ data={jsonValue}
183
+ theme={jsonTreeTheme}
184
+ invertTheme={false}
185
+ shouldExpandNodeInitially={(keyPath) => keyPath.length <= 2}
186
+ />
187
+ ) : (
188
+ <div className="text-sm">{formatValue(entry)}</div>
189
+ )}
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ {/* Dialog Actions */}
195
+ <div className="flex items-center justify-end gap-2 mt-6">
196
+ {onEdit && (
197
+ <button
198
+ onClick={() => {
199
+ if (entry && onEdit) {
200
+ onEdit(entry);
201
+ }
202
+ }}
203
+ className="flex items-center gap-2 px-4 py-2 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded transition-colors"
204
+ >
205
+ <Edit3 className="h-4 w-4" />
206
+ Edit
207
+ </button>
208
+ )}
209
+ <button
210
+ onClick={onClose}
211
+ className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
212
+ autoFocus
213
+ >
214
+ Close
215
+ </button>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ );
220
+ };
@@ -0,0 +1,123 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer utilities {
6
+ .text-balance {
7
+ text-wrap: balance;
8
+ }
9
+
10
+ .wrap-anywhere {
11
+ overflow-wrap: anywhere;
12
+ }
13
+ }
14
+
15
+ @layer base {
16
+ :root {
17
+ --background: 0 0% 100%;
18
+ --foreground: 0 0% 3.9%;
19
+ --card: 0 0% 100%;
20
+ --card-foreground: 0 0% 3.9%;
21
+ --popover: 0 0% 100%;
22
+ --popover-foreground: 0 0% 3.9%;
23
+ --primary: 0 0% 9%;
24
+ --primary-foreground: 0 0% 98%;
25
+ --secondary: 0 0% 96.1%;
26
+ --secondary-foreground: 0 0% 9%;
27
+ --muted: 0 0% 96.1%;
28
+ --muted-foreground: 0 0% 45.1%;
29
+ --accent: 0 0% 96.1%;
30
+ --accent-foreground: 0 0% 9%;
31
+ --destructive: 0 84.2% 60.2%;
32
+ --destructive-foreground: 0 0% 98%;
33
+ --border: 0 0% 89.8%;
34
+ --input: 0 0% 89.8%;
35
+ --ring: 0 0% 3.9%;
36
+ --chart-1: 12 76% 61%;
37
+ --chart-2: 173 58% 39%;
38
+ --chart-3: 197 37% 24%;
39
+ --chart-4: 43 74% 66%;
40
+ --chart-5: 27 87% 67%;
41
+ --radius: 0.5rem;
42
+ --sidebar-background: 0 0% 98%;
43
+ --sidebar-foreground: 240 5.3% 26.1%;
44
+ --sidebar-primary: 240 5.9% 10%;
45
+ --sidebar-primary-foreground: 0 0% 98%;
46
+ --sidebar-accent: 240 4.8% 95.9%;
47
+ --sidebar-accent-foreground: 240 5.9% 10%;
48
+ --sidebar-border: 220 13% 91%;
49
+ --sidebar-ring: 217.2 91.2% 59.8%;
50
+ }
51
+ .dark {
52
+ --background: 0 0% 3.9%;
53
+ --foreground: 0 0% 98%;
54
+ --card: 0 0% 3.9%;
55
+ --card-foreground: 0 0% 98%;
56
+ --popover: 0 0% 3.9%;
57
+ --popover-foreground: 0 0% 98%;
58
+ --primary: 0 0% 98%;
59
+ --primary-foreground: 0 0% 9%;
60
+ --secondary: 0 0% 14.9%;
61
+ --secondary-foreground: 0 0% 98%;
62
+ --muted: 0 0% 14.9%;
63
+ --muted-foreground: 0 0% 63.9%;
64
+ --accent: 0 0% 14.9%;
65
+ --accent-foreground: 0 0% 98%;
66
+ --destructive: 0 62.8% 30.6%;
67
+ --destructive-foreground: 0 0% 98%;
68
+ --border: 0 0% 14.9%;
69
+ --input: 0 0% 14.9%;
70
+ --ring: 0 0% 83.1%;
71
+ --chart-1: 220 70% 50%;
72
+ --chart-2: 160 60% 45%;
73
+ --chart-3: 30 80% 55%;
74
+ --chart-4: 280 65% 60%;
75
+ --chart-5: 340 75% 55%;
76
+ --sidebar-background: 240 5.9% 10%;
77
+ --sidebar-foreground: 240 4.8% 95.9%;
78
+ --sidebar-primary: 224.3 76.3% 48%;
79
+ --sidebar-primary-foreground: 0 0% 100%;
80
+ --sidebar-accent: 240 3.7% 15.9%;
81
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
82
+ --sidebar-border: 240 3.7% 15.9%;
83
+ --sidebar-ring: 217.2 91.2% 59.8%;
84
+ }
85
+ }
86
+
87
+ @layer base {
88
+ * {
89
+ @apply border-border;
90
+ }
91
+ body {
92
+ @apply bg-background text-foreground;
93
+ }
94
+ }
95
+
96
+ /* Custom scrollbar styles to match plugin theme */
97
+ ::-webkit-scrollbar {
98
+ width: 8px;
99
+ height: 8px;
100
+ }
101
+
102
+ ::-webkit-scrollbar-track {
103
+ background: #1f2937; /* gray-800 */
104
+ border-radius: 4px;
105
+ }
106
+
107
+ ::-webkit-scrollbar-thumb {
108
+ background: #374151; /* gray-700 */
109
+ border-radius: 4px;
110
+ border: 1px solid #1f2937; /* gray-800 */
111
+ }
112
+
113
+ ::-webkit-scrollbar-thumb:hover {
114
+ background: #4b5563; /* gray-600 */
115
+ }
116
+
117
+ ::-webkit-scrollbar-thumb:active {
118
+ background: #6b7280; /* gray-500 */
119
+ }
120
+
121
+ ::-webkit-scrollbar-corner {
122
+ background: #1f2937; /* gray-800 */
123
+ }