@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,310 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import type {
4
+ StorageEntry,
5
+ StorageEntryType,
6
+ StorageEntryValue,
7
+ } from '../shared/types';
8
+ import { ConfirmDialog } from './confirm-dialog';
9
+
10
+ const TYPE_OPTIONS: Array<{ value: StorageEntryType; label: string }> = [
11
+ { value: 'string', label: 'String' },
12
+ { value: 'number', label: 'Number' },
13
+ { value: 'boolean', label: 'Boolean' },
14
+ { value: 'buffer', label: 'Buffer (Array)' },
15
+ ];
16
+
17
+ export type AddEntryDialogProps = {
18
+ isOpen: boolean;
19
+ onClose: () => void;
20
+ onAddEntry: (entry: StorageEntry) => void;
21
+ existingKeys: string[];
22
+ supportedTypes: StorageEntryType[];
23
+ };
24
+
25
+ export const AddEntryDialog = ({
26
+ isOpen,
27
+ onClose,
28
+ onAddEntry,
29
+ existingKeys,
30
+ supportedTypes,
31
+ }: AddEntryDialogProps) => {
32
+ const [newEntryKey, setNewEntryKey] = useState('');
33
+ const [newEntryType, setNewEntryType] = useState<StorageEntryType>('string');
34
+ const [newEntryValue, setNewEntryValue] = useState('');
35
+ const [confirmDialog, setConfirmDialog] = useState<{
36
+ isOpen: boolean;
37
+ title: string;
38
+ message: string;
39
+ type: 'confirm' | 'alert';
40
+ onConfirm?: () => void;
41
+ }>({ isOpen: false, title: '', message: '', type: 'alert' });
42
+
43
+ const enabledTypes = useMemo(
44
+ () => TYPE_OPTIONS.filter((option) => supportedTypes.includes(option.value)),
45
+ [supportedTypes]
46
+ );
47
+
48
+ const firstSupportedType = enabledTypes[0]?.value;
49
+
50
+ const selectedTypeSupported = supportedTypes.includes(newEntryType);
51
+
52
+ const resetForm = () => {
53
+ setNewEntryKey('');
54
+ setNewEntryType(firstSupportedType ?? 'string');
55
+ setNewEntryValue('');
56
+ onClose();
57
+ };
58
+
59
+ const handleAddEntry = () => {
60
+ if (!newEntryKey.trim()) return;
61
+
62
+ if (!selectedTypeSupported) {
63
+ setConfirmDialog({
64
+ isOpen: true,
65
+ title: 'Unsupported Type',
66
+ message: 'Selected type is not supported by this storage.',
67
+ type: 'alert',
68
+ });
69
+ return;
70
+ }
71
+
72
+ if (existingKeys.includes(newEntryKey)) {
73
+ setConfirmDialog({
74
+ isOpen: true,
75
+ title: 'Key Already Exists',
76
+ message: 'An entry with this key already exists.',
77
+ type: 'alert',
78
+ });
79
+ return;
80
+ }
81
+
82
+ let parsedValue: StorageEntryValue;
83
+ try {
84
+ switch (newEntryType) {
85
+ case 'string':
86
+ parsedValue = newEntryValue;
87
+ break;
88
+ case 'number':
89
+ parsedValue = Number(newEntryValue);
90
+ if (Number.isNaN(parsedValue)) {
91
+ throw new Error('Invalid number');
92
+ }
93
+ break;
94
+ case 'boolean':
95
+ if (newEntryValue !== 'true' && newEntryValue !== 'false') {
96
+ throw new Error('Boolean value must be true or false');
97
+ }
98
+ parsedValue = newEntryValue === 'true';
99
+ break;
100
+ case 'buffer':
101
+ parsedValue = JSON.parse(newEntryValue);
102
+ if (
103
+ !Array.isArray(parsedValue) ||
104
+ !parsedValue.every((value) => typeof value === 'number')
105
+ ) {
106
+ throw new Error('Buffer must be an array of numbers');
107
+ }
108
+ break;
109
+ default:
110
+ throw new Error('Invalid type');
111
+ }
112
+ } catch (error) {
113
+ setConfirmDialog({
114
+ isOpen: true,
115
+ title: 'Invalid Value',
116
+ message: `Invalid value for ${newEntryType}: ${
117
+ error instanceof Error ? error.message : 'Unknown error'
118
+ }`,
119
+ type: 'alert',
120
+ });
121
+ return;
122
+ }
123
+
124
+ let entry: StorageEntry;
125
+ if (newEntryType === 'string') {
126
+ entry = { key: newEntryKey, type: 'string', value: parsedValue as string };
127
+ } else if (newEntryType === 'number') {
128
+ entry = { key: newEntryKey, type: 'number', value: parsedValue as number };
129
+ } else if (newEntryType === 'boolean') {
130
+ entry = { key: newEntryKey, type: 'boolean', value: parsedValue as boolean };
131
+ } else {
132
+ entry = { key: newEntryKey, type: 'buffer', value: parsedValue as number[] };
133
+ }
134
+
135
+ onAddEntry(entry);
136
+
137
+ resetForm();
138
+ };
139
+
140
+ const handleKeyDown = (event: React.KeyboardEvent) => {
141
+ if (event.key === 'Escape') {
142
+ resetForm();
143
+ return;
144
+ }
145
+
146
+ if (event.key === 'Enter' && newEntryKey.trim() && newEntryValue.trim()) {
147
+ handleAddEntry();
148
+ }
149
+ };
150
+
151
+ if (!isOpen) {
152
+ return null;
153
+ }
154
+
155
+ return (
156
+ <div
157
+ className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
158
+ onClick={resetForm}
159
+ >
160
+ <div
161
+ className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
162
+ onClick={(event) => event.stopPropagation()}
163
+ onKeyDown={handleKeyDown}
164
+ >
165
+ <div className="flex items-center justify-between mb-4">
166
+ <h2 className="text-lg font-semibold text-gray-100">Add New Entry</h2>
167
+ <button
168
+ onClick={resetForm}
169
+ className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
170
+ title="Close dialog"
171
+ >
172
+ <X className="h-4 w-4" />
173
+ </button>
174
+ </div>
175
+
176
+ <div className="space-y-4">
177
+ <div>
178
+ <label
179
+ htmlFor="new-entry-key"
180
+ className="block text-sm font-medium text-gray-200 mb-1"
181
+ >
182
+ Key
183
+ </label>
184
+ <input
185
+ id="new-entry-key"
186
+ type="text"
187
+ value={newEntryKey}
188
+ onChange={(event) => setNewEntryKey(event.target.value)}
189
+ placeholder="Enter key name"
190
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
191
+ autoFocus
192
+ />
193
+ </div>
194
+
195
+ <div>
196
+ <label
197
+ htmlFor="new-entry-type"
198
+ className="block text-sm font-medium text-gray-200 mb-1"
199
+ >
200
+ Type
201
+ </label>
202
+ <select
203
+ id="new-entry-type"
204
+ value={newEntryType}
205
+ onChange={(event) => {
206
+ setNewEntryType(event.target.value as StorageEntryType);
207
+ setNewEntryValue('');
208
+ }}
209
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
210
+ >
211
+ {TYPE_OPTIONS.map((option) => {
212
+ const isSupported = supportedTypes.includes(option.value);
213
+ return (
214
+ <option
215
+ key={option.value}
216
+ value={option.value}
217
+ disabled={!isSupported}
218
+ title={
219
+ isSupported ? option.label : 'Not supported by this storage'
220
+ }
221
+ >
222
+ {option.label}
223
+ {!isSupported ? ' (Not supported)' : ''}
224
+ </option>
225
+ );
226
+ })}
227
+ </select>
228
+ {!selectedTypeSupported && (
229
+ <p className="text-xs text-amber-400 mt-1">
230
+ Selected type is not supported by this storage.
231
+ </p>
232
+ )}
233
+ </div>
234
+
235
+ <div>
236
+ <label
237
+ htmlFor="new-entry-value"
238
+ className="block text-sm font-medium text-gray-200 mb-1"
239
+ >
240
+ Value
241
+ </label>
242
+ {newEntryType === 'boolean' ? (
243
+ <select
244
+ id="new-entry-value"
245
+ value={newEntryValue}
246
+ onChange={(event) => setNewEntryValue(event.target.value)}
247
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
248
+ >
249
+ <option value="">Select value</option>
250
+ <option value="true">true</option>
251
+ <option value="false">false</option>
252
+ </select>
253
+ ) : (
254
+ <input
255
+ id="new-entry-value"
256
+ type={newEntryType === 'number' ? 'number' : 'text'}
257
+ value={newEntryValue}
258
+ onChange={(event) => setNewEntryValue(event.target.value)}
259
+ placeholder={
260
+ newEntryType === 'string'
261
+ ? 'Enter string value'
262
+ : newEntryType === 'number'
263
+ ? 'Enter number value'
264
+ : newEntryType === 'buffer'
265
+ ? 'Enter array as JSON, e.g., [1, 2, 3]'
266
+ : 'Enter value'
267
+ }
268
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
269
+ />
270
+ )}
271
+ {newEntryType === 'buffer' && (
272
+ <p className="text-xs text-gray-400 mt-1">
273
+ Enter as JSON array of numbers, e.g., [1, 2, 3, 255]
274
+ </p>
275
+ )}
276
+ </div>
277
+ </div>
278
+
279
+ <div className="flex items-center justify-end gap-2 mt-6">
280
+ <button
281
+ onClick={resetForm}
282
+ className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
283
+ >
284
+ Cancel
285
+ </button>
286
+ <button
287
+ onClick={handleAddEntry}
288
+ disabled={!newEntryKey.trim() || !newEntryValue.trim() || !selectedTypeSupported}
289
+ className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded transition-colors"
290
+ >
291
+ Add Entry
292
+ </button>
293
+ </div>
294
+ </div>
295
+
296
+ <ConfirmDialog
297
+ isOpen={confirmDialog.isOpen}
298
+ onClose={() => setConfirmDialog((previous) => ({ ...previous, isOpen: false }))}
299
+ onConfirm={() => {
300
+ if (confirmDialog.onConfirm) {
301
+ confirmDialog.onConfirm();
302
+ }
303
+ }}
304
+ title={confirmDialog.title}
305
+ message={confirmDialog.message}
306
+ type={confirmDialog.type}
307
+ />
308
+ </div>
309
+ );
310
+ };
@@ -0,0 +1,100 @@
1
+ import { X, AlertTriangle, Info } from 'lucide-react';
2
+
3
+ export type ConfirmDialogProps = {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ onConfirm: () => void;
7
+ title: string;
8
+ message: string;
9
+ type?: 'confirm' | 'alert';
10
+ confirmText?: string;
11
+ cancelText?: string;
12
+ };
13
+
14
+ export const ConfirmDialog = ({
15
+ isOpen,
16
+ onClose,
17
+ onConfirm,
18
+ title,
19
+ message,
20
+ type = 'confirm',
21
+ confirmText = 'Confirm',
22
+ cancelText = 'Cancel',
23
+ }: ConfirmDialogProps) => {
24
+ const handleKeyDown = (e: React.KeyboardEvent) => {
25
+ if (e.key === 'Escape') {
26
+ onClose();
27
+ } else if (e.key === 'Enter') {
28
+ onConfirm();
29
+ }
30
+ };
31
+
32
+ const handleConfirm = () => {
33
+ onConfirm();
34
+ onClose();
35
+ };
36
+
37
+ const handleCancel = () => {
38
+ onClose();
39
+ };
40
+
41
+ if (!isOpen) return null;
42
+
43
+ return (
44
+ <div
45
+ className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
46
+ onClick={handleCancel}
47
+ >
48
+ <div
49
+ className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
50
+ onClick={(e) => e.stopPropagation()}
51
+ onKeyDown={handleKeyDown}
52
+ >
53
+ <div className="flex items-center justify-between mb-4">
54
+ <div className="flex items-center gap-2">
55
+ {type === 'confirm' ? (
56
+ <AlertTriangle className="h-5 w-5 text-yellow-500" />
57
+ ) : (
58
+ <Info className="h-5 w-5 text-blue-500" />
59
+ )}
60
+ <h2 className="text-lg font-semibold text-gray-100">{title}</h2>
61
+ </div>
62
+ <button
63
+ onClick={handleCancel}
64
+ className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
65
+ title="Close dialog"
66
+ >
67
+ <X className="h-4 w-4" />
68
+ </button>
69
+ </div>
70
+
71
+ <div className="mb-6">
72
+ <p className="text-sm text-gray-300 leading-relaxed">{message}</p>
73
+ </div>
74
+
75
+ {/* Dialog Actions */}
76
+ <div className="flex items-center justify-end gap-2">
77
+ {type === 'confirm' && (
78
+ <button
79
+ onClick={handleCancel}
80
+ className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
81
+ >
82
+ {cancelText}
83
+ </button>
84
+ )}
85
+ <button
86
+ onClick={handleConfirm}
87
+ className={`px-4 py-2 text-sm text-white rounded transition-colors ${
88
+ type === 'confirm'
89
+ ? 'bg-red-600 hover:bg-red-700'
90
+ : 'bg-blue-600 hover:bg-blue-700'
91
+ }`}
92
+ autoFocus
93
+ >
94
+ {type === 'alert' ? 'OK' : confirmText}
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ );
100
+ };
@@ -0,0 +1,280 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { X, Edit3 } from 'lucide-react';
3
+ import type {
4
+ StorageEntry,
5
+ StorageEntryType,
6
+ StorageEntryValue,
7
+ } from '../shared/types';
8
+ import { ConfirmDialog } from './confirm-dialog';
9
+
10
+ export type EditEntryDialogProps = {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ onEditEntry: (key: string, newValue: StorageEntryValue) => void;
14
+ entry: StorageEntry | null;
15
+ supportedTypes: StorageEntryType[];
16
+ };
17
+
18
+ export const EditEntryDialog = ({
19
+ isOpen,
20
+ onClose,
21
+ onEditEntry,
22
+ entry,
23
+ supportedTypes,
24
+ }: EditEntryDialogProps) => {
25
+ const [editValue, setEditValue] = useState('');
26
+ const [confirmDialog, setConfirmDialog] = useState<{
27
+ isOpen: boolean;
28
+ title: string;
29
+ message: string;
30
+ type: 'confirm' | 'alert';
31
+ onConfirm?: () => void;
32
+ }>({ isOpen: false, title: '', message: '', type: 'alert' });
33
+
34
+ useEffect(() => {
35
+ if (entry && isOpen) {
36
+ setEditValue(Array.isArray(entry.value) ? JSON.stringify(entry.value) : String(entry.value));
37
+ }
38
+ }, [entry, isOpen]);
39
+
40
+ const isTypeSupported = useMemo(
41
+ () => !!entry && supportedTypes.includes(entry.type),
42
+ [entry, supportedTypes]
43
+ );
44
+
45
+ const resetForm = () => {
46
+ setEditValue('');
47
+ onClose();
48
+ };
49
+
50
+ const handleEditEntry = () => {
51
+ if (!entry) return;
52
+
53
+ if (!isTypeSupported) {
54
+ setConfirmDialog({
55
+ isOpen: true,
56
+ title: 'Unsupported Type',
57
+ message: 'This storage does not support the current entry type.',
58
+ type: 'alert',
59
+ });
60
+ return;
61
+ }
62
+
63
+ let newValue: StorageEntryValue;
64
+
65
+ try {
66
+ switch (entry.type) {
67
+ case 'string':
68
+ newValue = editValue;
69
+ break;
70
+ case 'number':
71
+ newValue = Number(editValue);
72
+ if (Number.isNaN(newValue)) {
73
+ throw new Error('Invalid number');
74
+ }
75
+ break;
76
+ case 'boolean':
77
+ if (editValue !== 'true' && editValue !== 'false') {
78
+ throw new Error('Boolean value must be "true" or "false"');
79
+ }
80
+ newValue = editValue === 'true';
81
+ break;
82
+ case 'buffer':
83
+ newValue = JSON.parse(editValue);
84
+ if (!Array.isArray(newValue) || !newValue.every((v) => typeof v === 'number')) {
85
+ throw new Error('Buffer must be an array of numbers');
86
+ }
87
+ break;
88
+ default:
89
+ throw new Error('Invalid type');
90
+ }
91
+ } catch (error) {
92
+ setConfirmDialog({
93
+ isOpen: true,
94
+ title: 'Invalid Value',
95
+ message: `Invalid value for ${entry.type}: ${
96
+ error instanceof Error ? error.message : 'Unknown error'
97
+ }`,
98
+ type: 'alert',
99
+ });
100
+ return;
101
+ }
102
+
103
+ onEditEntry(entry.key, newValue);
104
+ resetForm();
105
+ };
106
+
107
+ const handleKeyDown = (event: React.KeyboardEvent) => {
108
+ if (event.key === 'Escape') {
109
+ resetForm();
110
+ } else if (event.key === 'Enter' && editValue.trim()) {
111
+ handleEditEntry();
112
+ }
113
+ };
114
+
115
+ const getInputType = (type: StorageEntryType) => {
116
+ if (type === 'number') {
117
+ return 'number';
118
+ }
119
+
120
+ return 'text';
121
+ };
122
+
123
+ const getPlaceholder = (type: StorageEntryType) => {
124
+ if (type === 'string') {
125
+ return 'Enter string value';
126
+ }
127
+
128
+ if (type === 'number') {
129
+ return 'Enter number value';
130
+ }
131
+
132
+ if (type === 'boolean') {
133
+ return 'Enter true or false';
134
+ }
135
+
136
+ return 'Enter array as JSON, e.g., [1, 2, 3]';
137
+ };
138
+
139
+ const getTypeColorClass = (type: StorageEntryType) => {
140
+ if (type === 'string') {
141
+ return 'bg-green-600';
142
+ }
143
+
144
+ if (type === 'number') {
145
+ return 'bg-blue-600';
146
+ }
147
+
148
+ if (type === 'boolean') {
149
+ return 'bg-yellow-600';
150
+ }
151
+
152
+ return 'bg-purple-600';
153
+ };
154
+
155
+ if (!isOpen || !entry) {
156
+ return null;
157
+ }
158
+
159
+ return (
160
+ <div
161
+ className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
162
+ onClick={resetForm}
163
+ >
164
+ <div
165
+ className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
166
+ onClick={(event) => event.stopPropagation()}
167
+ onKeyDown={handleKeyDown}
168
+ >
169
+ <div className="flex items-center justify-between mb-4">
170
+ <div className="flex items-center gap-2">
171
+ <Edit3 className="h-5 w-5 text-blue-400" />
172
+ <h2 className="text-lg font-semibold text-gray-100">Edit Entry</h2>
173
+ </div>
174
+ <button
175
+ onClick={resetForm}
176
+ className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
177
+ title="Close dialog"
178
+ >
179
+ <X className="h-4 w-4" />
180
+ </button>
181
+ </div>
182
+
183
+ <div className="space-y-4">
184
+ <div>
185
+ <label className="block text-sm font-medium text-gray-200 mb-1">Key</label>
186
+ <div className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 font-mono">
187
+ {entry.key}
188
+ </div>
189
+ <p className="text-xs text-gray-400 mt-1">Key cannot be changed during editing</p>
190
+ </div>
191
+
192
+ <div>
193
+ <label className="block text-sm font-medium text-gray-200 mb-1">Type</label>
194
+ <div className="flex items-center">
195
+ <span
196
+ className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
197
+ entry.type
198
+ )}`}
199
+ >
200
+ {entry.type}
201
+ </span>
202
+ </div>
203
+ {!isTypeSupported ? (
204
+ <p className="text-xs text-amber-400 mt-1">
205
+ This storage does not support {entry.type} values.
206
+ </p>
207
+ ) : (
208
+ <p className="text-xs text-gray-400 mt-1">Type cannot be changed during editing</p>
209
+ )}
210
+ </div>
211
+
212
+ <div>
213
+ <label
214
+ htmlFor="edit-entry-value"
215
+ className="block text-sm font-medium text-gray-200 mb-1"
216
+ >
217
+ Value
218
+ </label>
219
+ {entry.type === 'boolean' ? (
220
+ <select
221
+ id="edit-entry-value"
222
+ value={editValue}
223
+ onChange={(event) => setEditValue(event.target.value)}
224
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
225
+ autoFocus
226
+ >
227
+ <option value="true">true</option>
228
+ <option value="false">false</option>
229
+ </select>
230
+ ) : (
231
+ <input
232
+ id="edit-entry-value"
233
+ type={getInputType(entry.type)}
234
+ value={editValue}
235
+ onChange={(event) => setEditValue(event.target.value)}
236
+ placeholder={getPlaceholder(entry.type)}
237
+ className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
238
+ autoFocus
239
+ />
240
+ )}
241
+ {entry.type === 'buffer' && (
242
+ <p className="text-xs text-gray-400 mt-1">
243
+ Enter as JSON array of numbers, e.g., [1, 2, 3, 255]
244
+ </p>
245
+ )}
246
+ </div>
247
+ </div>
248
+
249
+ <div className="flex items-center justify-end gap-2 mt-6">
250
+ <button
251
+ onClick={resetForm}
252
+ className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
253
+ >
254
+ Cancel
255
+ </button>
256
+ <button
257
+ onClick={handleEditEntry}
258
+ disabled={!editValue.trim() || !isTypeSupported}
259
+ className="px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded transition-colors"
260
+ >
261
+ Save Changes
262
+ </button>
263
+ </div>
264
+ </div>
265
+
266
+ <ConfirmDialog
267
+ isOpen={confirmDialog.isOpen}
268
+ onClose={() => setConfirmDialog((previous) => ({ ...previous, isOpen: false }))}
269
+ onConfirm={() => {
270
+ if (confirmDialog.onConfirm) {
271
+ confirmDialog.onConfirm();
272
+ }
273
+ }}
274
+ title={confirmDialog.title}
275
+ message={confirmDialog.message}
276
+ type={confirmDialog.type}
277
+ />
278
+ </div>
279
+ );
280
+ };