@rozenite/storage-plugin 1.8.1 → 1.10.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 (42) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +23 -0
  3. package/dist/devtools/assets/panel-Bm-SWF7d.js +33 -0
  4. package/dist/devtools/assets/panel-DIqI4WSp.css +1 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/index.require.cjs +1 -1
  7. package/dist/react-native/chunks/index.require.js +43 -57
  8. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  9. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +253 -191
  10. package/dist/react-native/index.d.ts +29 -6
  11. package/dist/rozenite.json +1 -1
  12. package/dist/sdk/index.d.ts +1 -0
  13. package/package.json +9 -6
  14. package/src/react-native/__tests__/import.test.ts +182 -0
  15. package/src/react-native/adapters/__tests__/mmkv.test.ts +436 -0
  16. package/src/react-native/adapters/mmkv.ts +65 -39
  17. package/src/react-native/import.ts +67 -0
  18. package/src/react-native/storage-view.ts +16 -8
  19. package/src/react-native/useRozeniteStoragePlugin.ts +61 -36
  20. package/src/shared/__tests__/snapshot.test.ts +361 -0
  21. package/src/shared/messaging.ts +25 -1
  22. package/src/shared/snapshot.ts +276 -0
  23. package/src/shared/types.ts +1 -0
  24. package/src/ui/__tests__/binary-value-editor-state.test.ts +199 -0
  25. package/src/ui/__tests__/binary.test.ts +251 -0
  26. package/src/ui/__tests__/type-conversion.test.ts +123 -0
  27. package/src/ui/add-entry-dialog.tsx +83 -173
  28. package/src/ui/binary-value-editor-state.ts +125 -0
  29. package/src/ui/binary-value-editor.tsx +168 -0
  30. package/src/ui/binary.ts +123 -0
  31. package/src/ui/edit-entry-dialog.tsx +64 -161
  32. package/src/ui/editable-table.tsx +12 -10
  33. package/src/ui/editor-switcher.tsx +62 -0
  34. package/src/ui/entry-detail-dialog.tsx +14 -6
  35. package/src/ui/import-dialog.tsx +261 -0
  36. package/src/ui/panel.tsx +257 -57
  37. package/src/ui/type-conversion.ts +105 -0
  38. package/src/ui/typed-value-editor.tsx +96 -0
  39. package/src/ui/utils.ts +30 -0
  40. package/dist/devtools/assets/panel-DMhXYHH4.css +0 -1
  41. package/dist/devtools/assets/panel-eGOuOVos.js +0 -22
  42. package/src/react-native/is-garbled.ts +0 -17
@@ -0,0 +1,261 @@
1
+ import { AlertTriangle, CheckCircle2, Loader2, X, XCircle } from 'lucide-react';
2
+ import type { StorageEntry, StorageTarget } from '../shared/types';
3
+ import type { ImportPreview, StorageSnapshotV1 } from '../shared/snapshot';
4
+
5
+ export type ImportFlightState =
6
+ | {
7
+ phase: 'preview';
8
+ target: StorageTarget;
9
+ targetLabel: string;
10
+ snapshot: StorageSnapshotV1;
11
+ preview: ImportPreview;
12
+ entriesToWrite: StorageEntry[];
13
+ }
14
+ | {
15
+ phase: 'importing';
16
+ target: StorageTarget;
17
+ total: number;
18
+ written: number;
19
+ }
20
+ | {
21
+ phase: 'result';
22
+ ok: true;
23
+ written: number;
24
+ }
25
+ | {
26
+ phase: 'result';
27
+ ok: false;
28
+ written: number;
29
+ total: number;
30
+ failedKey?: string;
31
+ error: string;
32
+ };
33
+
34
+ export type ImportDialogProps = {
35
+ state: ImportFlightState | null;
36
+ onApply: () => void;
37
+ onCancel: () => void;
38
+ onClose: () => void;
39
+ };
40
+
41
+ const KeyList = ({ title, items }: { title: string; items: string[] }) => {
42
+ if (items.length === 0) return null;
43
+ return (
44
+ <div>
45
+ <div className="text-xs font-medium text-gray-300">
46
+ {title} ({items.length})
47
+ </div>
48
+ <div className="mt-1 max-h-24 overflow-auto rounded bg-gray-900 px-2 py-1 font-mono text-xs text-gray-200">
49
+ {items.slice(0, 50).join(', ')}
50
+ {items.length > 50 ? `, +${items.length - 50} more` : ''}
51
+ </div>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ const PreviewBody = ({
57
+ state,
58
+ onApply,
59
+ onCancel,
60
+ }: {
61
+ state: Extract<ImportFlightState, { phase: 'preview' }>;
62
+ onApply: () => void;
63
+ onCancel: () => void;
64
+ }) => {
65
+ const { snapshot, preview, entriesToWrite, targetLabel } = state;
66
+ const hasUnsupported = preview.unsupportedTypes.length > 0;
67
+ const sourceLabel = `${snapshot.storage.adapterName} / ${snapshot.storage.storageName}`;
68
+
69
+ return (
70
+ <>
71
+ <div className="space-y-3">
72
+ {preview.metadataMismatch && (
73
+ <div className="flex items-start gap-2 rounded border border-yellow-700 bg-yellow-900/30 p-2 text-xs text-yellow-200">
74
+ <AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0" />
75
+ <div>
76
+ This file was exported from <strong>{sourceLabel}</strong>. You
77
+ are importing into <strong>{targetLabel}</strong>.
78
+ </div>
79
+ </div>
80
+ )}
81
+
82
+ {hasUnsupported && (
83
+ <div className="flex items-start gap-2 rounded border border-red-700 bg-red-900/30 p-2 text-xs text-red-200">
84
+ <XCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
85
+ <div>
86
+ {preview.unsupportedTypes.length}{' '}
87
+ {preview.unsupportedTypes.length === 1
88
+ ? 'entry has a type'
89
+ : 'entries have types'}{' '}
90
+ not supported by this storage. Remove them from the file and try
91
+ again.
92
+ <div className="mt-1 max-h-20 overflow-auto rounded bg-gray-900 px-2 py-1 font-mono text-xs text-red-100">
93
+ {preview.unsupportedTypes
94
+ .map((u) => `${u.key} (${u.type})`)
95
+ .join(', ')}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ )}
100
+
101
+ <div className="grid grid-cols-3 gap-2 text-xs">
102
+ <div className="rounded bg-gray-900 p-2">
103
+ <div className="text-gray-400">New</div>
104
+ <div className="text-base font-semibold text-green-400">
105
+ {preview.newKeys.length}
106
+ </div>
107
+ </div>
108
+ <div className="rounded bg-gray-900 p-2">
109
+ <div className="text-gray-400">Overwrite</div>
110
+ <div className="text-base font-semibold text-yellow-400">
111
+ {preview.overwriteKeys.length}
112
+ </div>
113
+ </div>
114
+ <div className="rounded bg-gray-900 p-2">
115
+ <div className="text-gray-400">Skipped</div>
116
+ <div className="text-base font-semibold text-gray-400">
117
+ {preview.skippedKeys.length}
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <KeyList title="New keys" items={preview.newKeys} />
123
+ <KeyList title="Overwrite keys" items={preview.overwriteKeys} />
124
+ <KeyList
125
+ title="Skipped (filtered by storage)"
126
+ items={preview.skippedKeys.map((s) => s.key)}
127
+ />
128
+ </div>
129
+
130
+ <div className="mt-6 flex items-center justify-end gap-2">
131
+ <button
132
+ onClick={onCancel}
133
+ className="rounded px-4 py-2 text-sm text-gray-300 transition-colors hover:bg-gray-700 hover:text-white"
134
+ >
135
+ Cancel
136
+ </button>
137
+ <button
138
+ onClick={onApply}
139
+ disabled={hasUnsupported}
140
+ className="rounded bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-600"
141
+ >
142
+ Apply ({entriesToWrite.length})
143
+ </button>
144
+ </div>
145
+ </>
146
+ );
147
+ };
148
+
149
+ const ImportingBody = ({
150
+ state,
151
+ }: {
152
+ state: Extract<ImportFlightState, { phase: 'importing' }>;
153
+ }) => (
154
+ <div className="flex flex-col items-center gap-3 py-6">
155
+ <Loader2 className="h-6 w-6 animate-spin text-blue-400" />
156
+ <div className="text-sm text-gray-200">
157
+ Importing… {state.written} / {state.total}
158
+ </div>
159
+ </div>
160
+ );
161
+
162
+ const ResultBody = ({
163
+ state,
164
+ onClose,
165
+ }: {
166
+ state: Extract<ImportFlightState, { phase: 'result' }>;
167
+ onClose: () => void;
168
+ }) => (
169
+ <>
170
+ {state.ok ? (
171
+ <div className="flex flex-col items-center gap-3 py-4">
172
+ <CheckCircle2 className="h-8 w-8 text-green-400" />
173
+ <div className="text-sm text-gray-200">
174
+ Imported {state.written} {state.written === 1 ? 'entry' : 'entries'}.
175
+ </div>
176
+ </div>
177
+ ) : (
178
+ <div className="space-y-2 py-2">
179
+ <div className="flex items-center gap-2 text-red-300">
180
+ <XCircle className="h-5 w-5" />
181
+ <div className="text-sm font-medium">
182
+ Import failed after {state.written} of {state.total}.
183
+ </div>
184
+ </div>
185
+ {state.failedKey && (
186
+ <div className="text-xs text-gray-300">
187
+ Failed at key:{' '}
188
+ <span className="font-mono text-gray-100">{state.failedKey}</span>
189
+ </div>
190
+ )}
191
+ <div className="rounded bg-gray-900 px-2 py-1 font-mono text-xs text-red-200">
192
+ {state.error}
193
+ </div>
194
+ </div>
195
+ )}
196
+
197
+ <div className="mt-4 flex items-center justify-end">
198
+ <button
199
+ onClick={onClose}
200
+ autoFocus
201
+ className="rounded bg-blue-600 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-700"
202
+ >
203
+ Close
204
+ </button>
205
+ </div>
206
+ </>
207
+ );
208
+
209
+ export const ImportDialog = ({
210
+ state,
211
+ onApply,
212
+ onCancel,
213
+ onClose,
214
+ }: ImportDialogProps) => {
215
+ if (!state) return null;
216
+
217
+ const title =
218
+ state.phase === 'preview'
219
+ ? 'Import snapshot'
220
+ : state.phase === 'importing'
221
+ ? 'Importing…'
222
+ : state.ok
223
+ ? 'Import complete'
224
+ : 'Import failed';
225
+
226
+ // Importing phase: dialog is locked; only the success/failure transition closes it.
227
+ const isDismissible = state.phase !== 'importing';
228
+
229
+ return (
230
+ <div
231
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
232
+ onClick={isDismissible ? onClose : undefined}
233
+ >
234
+ <div
235
+ className="mx-4 w-[28rem] max-w-full rounded-lg bg-gray-800 p-6"
236
+ onClick={(event) => event.stopPropagation()}
237
+ >
238
+ <div className="mb-4 flex items-center justify-between">
239
+ <h2 className="text-lg font-semibold text-gray-100">{title}</h2>
240
+ {isDismissible && (
241
+ <button
242
+ onClick={onClose}
243
+ className="rounded p-1 text-gray-400 transition-colors hover:bg-gray-700 hover:text-gray-200"
244
+ title="Close dialog"
245
+ >
246
+ <X className="h-4 w-4" />
247
+ </button>
248
+ )}
249
+ </div>
250
+
251
+ {state.phase === 'preview' && (
252
+ <PreviewBody state={state} onApply={onApply} onCancel={onCancel} />
253
+ )}
254
+ {state.phase === 'importing' && <ImportingBody state={state} />}
255
+ {state.phase === 'result' && (
256
+ <ResultBody state={state} onClose={onClose} />
257
+ )}
258
+ </div>
259
+ </div>
260
+ );
261
+ };