@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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +20 -0
- package/README.md +77 -0
- package/dist/assets/panel-C7usUotG.js +22 -0
- package/dist/assets/panel-DMhXYHH4.css +1 -0
- package/dist/panel.html +31 -0
- package/dist/react-native.cjs +1 -0
- package/dist/react-native.d.ts +4 -0
- package/dist/react-native.js +200 -0
- package/dist/rozenite.config.d.ts +7 -0
- package/dist/rozenite.json +1 -0
- package/dist/src/react-native/adapters/async-storage.d.ts +33 -0
- package/dist/src/react-native/adapters/index.d.ts +6 -0
- package/dist/src/react-native/adapters/mmkv.d.ts +13 -0
- package/dist/src/react-native/adapters/secure-storage.d.ts +18 -0
- package/dist/src/react-native/is-garbled.d.ts +1 -0
- package/dist/src/react-native/storage-view.d.ts +19 -0
- package/dist/src/react-native/useRozeniteStoragePlugin.d.ts +6 -0
- package/dist/src/shared/messaging.d.ts +29 -0
- package/dist/src/shared/types.d.ts +60 -0
- package/dist/src/ui/add-entry-dialog.d.ts +9 -0
- package/dist/src/ui/confirm-dialog.d.ts +11 -0
- package/dist/src/ui/edit-entry-dialog.d.ts +9 -0
- package/dist/src/ui/editable-table.d.ts +10 -0
- package/dist/src/ui/entry-detail-dialog.d.ts +8 -0
- package/dist/src/ui/panel.d.ts +1 -0
- package/dist/types.cjs +1 -0
- package/dist/types.js +11 -0
- package/dist/useRozeniteStoragePlugin.cjs +1 -0
- package/dist/useRozeniteStoragePlugin.js +212 -0
- package/package.json +59 -0
- package/postcss.config.js +6 -0
- package/react-native.ts +40 -0
- package/rozenite.config.ts +8 -0
- package/src/css-modules.d.ts +4 -0
- package/src/react-native/adapters/async-storage.ts +115 -0
- package/src/react-native/adapters/index.ts +19 -0
- package/src/react-native/adapters/mmkv.ts +187 -0
- package/src/react-native/adapters/secure-storage.ts +76 -0
- package/src/react-native/is-garbled.ts +17 -0
- package/src/react-native/storage-view.ts +245 -0
- package/src/react-native/useRozeniteStoragePlugin.ts +162 -0
- package/src/shared/messaging.ts +37 -0
- package/src/shared/types.ts +66 -0
- package/src/ui/add-entry-dialog.tsx +310 -0
- package/src/ui/confirm-dialog.tsx +100 -0
- package/src/ui/edit-entry-dialog.tsx +280 -0
- package/src/ui/editable-table.tsx +298 -0
- package/src/ui/entry-detail-dialog.tsx +220 -0
- package/src/ui/globals.css +123 -0
- package/src/ui/panel.tsx +442 -0
- package/tailwind.config.ts +94 -0
- package/tsconfig.json +36 -0
- 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
|
+
}
|