@rozenite/storage-plugin 1.13.0 → 2.1.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 (84) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
  4. package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
  5. package/dist/devtools/panel.html +2 -2
  6. package/dist/react-native/chunks/async-storage.require.cjs +1 -1
  7. package/dist/react-native/chunks/async-storage.require.js +8 -7
  8. package/dist/react-native/chunks/index.require.cjs +1 -1
  9. package/dist/react-native/chunks/index.require.js +70 -66
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +17 -13
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
  14. package/dist/react-native/index.d.ts +166 -18
  15. package/dist/rozenite.json +1 -1
  16. package/dist/sdk/index.cjs +1 -1
  17. package/dist/sdk/index.d.ts +13 -5
  18. package/dist/sdk/index.js +21 -12
  19. package/package.json +35 -37
  20. package/react-native.ts +12 -22
  21. package/rozenite.config.ts +74 -0
  22. package/sdk.ts +1 -5
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
  26. package/src/react-native/__tests__/import.test.ts +174 -11
  27. package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +140 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
  31. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  32. package/src/react-native/adapters/async-storage.ts +8 -9
  33. package/src/react-native/adapters/index.ts +2 -8
  34. package/src/react-native/adapters/mmkv.ts +12 -24
  35. package/src/react-native/adapters/secure-storage.ts +4 -0
  36. package/src/react-native/entry-preview-pagination.ts +261 -0
  37. package/src/react-native/export-snapshot.ts +88 -0
  38. package/src/react-native/full-entry-request.ts +91 -0
  39. package/src/react-native/import.ts +179 -16
  40. package/src/react-native/storage-discovery.ts +92 -0
  41. package/src/react-native/storage-view.ts +47 -109
  42. package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
  43. package/src/react-native/useStorageAgentTools.ts +196 -91
  44. package/src/shared/__tests__/entry-preview.test.ts +99 -0
  45. package/src/shared/__tests__/snapshot.test.ts +31 -32
  46. package/src/shared/agent-tools.ts +33 -38
  47. package/src/shared/entry-preview.ts +54 -0
  48. package/src/shared/messaging.ts +140 -21
  49. package/src/shared/snapshot.ts +12 -18
  50. package/src/shared/types.ts +21 -4
  51. package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
  52. package/src/ui/__tests__/binary.test.ts +31 -33
  53. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  54. package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
  55. package/src/ui/__tests__/panel.test.tsx +382 -0
  56. package/src/ui/__tests__/storage-groups.test.ts +88 -0
  57. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  58. package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
  59. package/src/ui/add-entry-dialog.tsx +66 -117
  60. package/src/ui/binary-value-editor-state.ts +16 -25
  61. package/src/ui/binary-value-editor.tsx +59 -47
  62. package/src/ui/binary.ts +54 -15
  63. package/src/ui/edit-entry-dialog.tsx +68 -104
  64. package/src/ui/editor-switcher.tsx +9 -16
  65. package/src/ui/entry-detail-dialog.tsx +62 -181
  66. package/src/ui/format-value.tsx +25 -0
  67. package/src/ui/globals.css +1 -123
  68. package/src/ui/import-dialog.tsx +72 -109
  69. package/src/ui/large-value-viewer-state.ts +25 -0
  70. package/src/ui/large-value-viewer.tsx +65 -0
  71. package/src/ui/panel.tsx +649 -509
  72. package/src/ui/query-client.tsx +23 -0
  73. package/src/ui/storage-groups.ts +66 -0
  74. package/src/ui/type-conversion.ts +2 -5
  75. package/src/ui/typed-value-editor.tsx +15 -20
  76. package/src/ui/use-storage-entries.ts +192 -0
  77. package/src/ui/utils.ts +1 -4
  78. package/tsconfig.json +5 -1
  79. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  80. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  81. package/postcss.config.js +0 -6
  82. package/src/ui/confirm-dialog.tsx +0 -100
  83. package/src/ui/editable-table.tsx +0 -300
  84. package/tailwind.config.ts +0 -94
@@ -1,4 +1,5 @@
1
- import { AlertTriangle, CheckCircle2, Loader2, X, XCircle } from 'lucide-react';
1
+ import { Badge, Button, Dialog } from '@rozenite/ui';
2
+ import { AlertTriangle, CheckCircle2, Loader2, XCircle } from 'lucide-react';
2
3
  import type { StorageEntry, StorageTarget } from '../shared/types';
3
4
  import type { ImportPreview, StorageSnapshotV1 } from '../shared/snapshot';
4
5
 
@@ -13,6 +14,7 @@ export type ImportFlightState =
13
14
  }
14
15
  | {
15
16
  phase: 'importing';
17
+ requestId: string;
16
18
  target: StorageTarget;
17
19
  total: number;
18
20
  written: number;
@@ -42,10 +44,10 @@ const KeyList = ({ title, items }: { title: string; items: string[] }) => {
42
44
  if (items.length === 0) return null;
43
45
  return (
44
46
  <div>
45
- <div className="text-xs font-medium text-gray-300">
47
+ <div className="text-xs font-medium text-foreground">
46
48
  {title} ({items.length})
47
49
  </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">
50
+ <div className="mt-1 max-h-24 overflow-auto rounded-md bg-muted px-2 py-1 font-mono text-xs text-muted-foreground">
49
51
  {items.slice(0, 50).join(', ')}
50
52
  {items.length > 50 ? `, +${items.length - 50} more` : ''}
51
53
  </div>
@@ -53,6 +55,13 @@ const KeyList = ({ title, items }: { title: string; items: string[] }) => {
53
55
  );
54
56
  };
55
57
 
58
+ const StatTile = ({ label, value }: { label: string; value: number }) => (
59
+ <div className="rounded-md bg-muted p-2">
60
+ <div className="text-muted-foreground">{label}</div>
61
+ <div className="text-base font-semibold text-foreground">{value}</div>
62
+ </div>
63
+ );
64
+
56
65
  const PreviewBody = ({
57
66
  state,
58
67
  onApply,
@@ -68,55 +77,35 @@ const PreviewBody = ({
68
77
 
69
78
  return (
70
79
  <>
71
- <div className="space-y-3">
80
+ <div className="flex flex-col gap-3">
72
81
  {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" />
82
+ <div className="flex items-start gap-2 rounded-md border border-border bg-muted p-2 text-xs text-foreground">
83
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
75
84
  <div>
76
- This file was exported from <strong>{sourceLabel}</strong>. You
77
- are importing into <strong>{targetLabel}</strong>.
85
+ This file was exported from <strong>{sourceLabel}</strong>. You are importing into{' '}
86
+ <strong>{targetLabel}</strong>.
78
87
  </div>
79
88
  </div>
80
89
  )}
81
90
 
82
91
  {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" />
92
+ <div className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-xs text-destructive">
93
+ <XCircle className="mt-0.5 h-4 w-4 shrink-0" />
85
94
  <div>
86
95
  {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
+ {preview.unsupportedTypes.length === 1 ? 'entry has a type' : 'entries have types'}{' '}
97
+ not supported by this storage. Remove them from the file and try again.
98
+ <div className="mt-1 max-h-20 overflow-auto rounded-md bg-muted px-2 py-1 font-mono text-xs">
99
+ {preview.unsupportedTypes.map((u) => `${u.key} (${u.type})`).join(', ')}
96
100
  </div>
97
101
  </div>
98
102
  </div>
99
103
  )}
100
104
 
101
105
  <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>
106
+ <StatTile label="New" value={preview.newKeys.length} />
107
+ <StatTile label="Overwrite" value={preview.overwriteKeys.length} />
108
+ <StatTile label="Skipped" value={preview.skippedKeys.length} />
120
109
  </div>
121
110
 
122
111
  <KeyList title="New keys" items={preview.newKeys} />
@@ -127,21 +116,14 @@ const PreviewBody = ({
127
116
  />
128
117
  </div>
129
118
 
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
- >
119
+ <Dialog.Footer>
120
+ <Button variant="outline" onClick={onCancel}>
135
121
  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
- >
122
+ </Button>
123
+ <Button onClick={onApply} disabled={hasUnsupported}>
142
124
  Apply ({entriesToWrite.length})
143
- </button>
144
- </div>
125
+ </Button>
126
+ </Dialog.Footer>
145
127
  </>
146
128
  );
147
129
  };
@@ -152,8 +134,8 @@ const ImportingBody = ({
152
134
  state: Extract<ImportFlightState, { phase: 'importing' }>;
153
135
  }) => (
154
136
  <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">
137
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
138
+ <div className="text-sm text-foreground">
157
139
  Importing… {state.written} / {state.total}
158
140
  </div>
159
141
  </div>
@@ -169,93 +151,74 @@ const ResultBody = ({
169
151
  <>
170
152
  {state.ok ? (
171
153
  <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">
154
+ <CheckCircle2 className="h-8 w-8 text-primary" />
155
+ <div className="text-sm text-foreground">
174
156
  Imported {state.written} {state.written === 1 ? 'entry' : 'entries'}.
175
157
  </div>
176
158
  </div>
177
159
  ) : (
178
- <div className="space-y-2 py-2">
179
- <div className="flex items-center gap-2 text-red-300">
160
+ <div className="flex flex-col gap-2 py-2">
161
+ <div className="flex items-center gap-2 text-destructive">
180
162
  <XCircle className="h-5 w-5" />
181
163
  <div className="text-sm font-medium">
182
164
  Import failed after {state.written} of {state.total}.
183
165
  </div>
184
166
  </div>
185
167
  {state.failedKey && (
186
- <div className="text-xs text-gray-300">
168
+ <div className="text-xs text-muted-foreground">
187
169
  Failed at key:{' '}
188
- <span className="font-mono text-gray-100">{state.failedKey}</span>
170
+ <Badge variant="outline" className="font-mono">
171
+ {state.failedKey}
172
+ </Badge>
189
173
  </div>
190
174
  )}
191
- <div className="rounded bg-gray-900 px-2 py-1 font-mono text-xs text-red-200">
175
+ <div className="rounded-md bg-muted px-2 py-1 font-mono text-xs text-destructive">
192
176
  {state.error}
193
177
  </div>
194
178
  </div>
195
179
  )}
196
180
 
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
- >
181
+ <Dialog.Footer>
182
+ <Button onClick={onClose} autoFocus>
203
183
  Close
204
- </button>
205
- </div>
184
+ </Button>
185
+ </Dialog.Footer>
206
186
  </>
207
187
  );
208
188
 
209
- export const ImportDialog = ({
210
- state,
211
- onApply,
212
- onCancel,
213
- onClose,
214
- }: ImportDialogProps) => {
215
- if (!state) return null;
216
-
189
+ export const ImportDialog = ({ state, onApply, onCancel, onClose }: ImportDialogProps) => {
217
190
  const title =
218
- state.phase === 'preview'
219
- ? 'Import snapshot'
220
- : state.phase === 'importing'
221
- ? 'Importing…'
222
- : state.ok
223
- ? 'Import complete'
224
- : 'Import failed';
191
+ state === null
192
+ ? ''
193
+ : state.phase === 'preview'
194
+ ? 'Import snapshot'
195
+ : state.phase === 'importing'
196
+ ? 'Importing…'
197
+ : state.ok
198
+ ? 'Import complete'
199
+ : 'Import failed';
225
200
 
226
201
  // Importing phase: dialog is locked; only the success/failure transition closes it.
227
- const isDismissible = state.phase !== 'importing';
202
+ const isDismissible = state !== null && state.phase !== 'importing';
228
203
 
229
204
  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}
205
+ <Dialog
206
+ open={state !== null}
207
+ onOpenChange={(open) => {
208
+ if (!open && isDismissible) onClose();
209
+ }}
233
210
  >
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>
211
+ <Dialog.Content showCloseButton={isDismissible}>
212
+ <Dialog.Header>
213
+ <Dialog.Title>{title}</Dialog.Title>
214
+ </Dialog.Header>
250
215
 
251
- {state.phase === 'preview' && (
216
+ {state?.phase === 'preview' && (
252
217
  <PreviewBody state={state} onApply={onApply} onCancel={onCancel} />
253
218
  )}
254
- {state.phase === 'importing' && <ImportingBody state={state} />}
255
- {state.phase === 'result' && (
256
- <ResultBody state={state} onClose={onClose} />
257
- )}
258
- </div>
259
- </div>
219
+ {state?.phase === 'importing' && <ImportingBody state={state} />}
220
+ {state?.phase === 'result' && <ResultBody state={state} onClose={onClose} />}
221
+ </Dialog.Content>
222
+ </Dialog>
260
223
  );
261
224
  };
@@ -0,0 +1,25 @@
1
+ export const VIRTUALIZED_TEXT_THRESHOLD = 50_000;
2
+ export const TEXT_CHUNK_SIZE = 2_000;
3
+
4
+ export type TextRange = readonly [start: number, end: number];
5
+
6
+ // Keep an index of row boundaries instead of splitting the complete value.
7
+ // This lets the virtualizer create text only for visible rows, including a
8
+ // single pathological line with no newline boundaries.
9
+ export const textRowRanges = (text: string): TextRange[] => {
10
+ const ranges: TextRange[] = [];
11
+ let start = 0;
12
+
13
+ while (start < text.length) {
14
+ const newline = text.indexOf('\n', start);
15
+ const lineEnd = newline === -1 ? text.length : newline + 1;
16
+
17
+ for (let chunkStart = start; chunkStart < lineEnd; chunkStart += TEXT_CHUNK_SIZE) {
18
+ ranges.push([chunkStart, Math.min(chunkStart + TEXT_CHUNK_SIZE, lineEnd)]);
19
+ }
20
+
21
+ start = lineEnd;
22
+ }
23
+
24
+ return ranges.length === 0 ? [[0, 0]] : ranges;
25
+ };
@@ -0,0 +1,65 @@
1
+ import { useMemo } from 'react';
2
+ import { Virtuoso } from 'react-virtuoso';
3
+ import { formatHexdumpRow } from './binary';
4
+ import { textRowRanges, VIRTUALIZED_TEXT_THRESHOLD } from './large-value-viewer-state';
5
+
6
+ export const TextValueViewer = ({ value }: { value: string }) => {
7
+ const ranges = useMemo(
8
+ () => (value.length > VIRTUALIZED_TEXT_THRESHOLD ? textRowRanges(value) : null),
9
+ [value],
10
+ );
11
+
12
+ if (!ranges) {
13
+ return (
14
+ <pre className="font-mono text-sm text-success whitespace-pre-wrap wrap-anywhere">
15
+ "{value}"
16
+ </pre>
17
+ );
18
+ }
19
+
20
+ return (
21
+ <Virtuoso
22
+ aria-label="Large string value"
23
+ className="font-mono text-sm text-success"
24
+ style={{ height: 384 }}
25
+ totalCount={ranges.length}
26
+ itemContent={(index) => {
27
+ const [start, end] = ranges[index];
28
+ return (
29
+ <div className="whitespace-pre-wrap wrap-anywhere">
30
+ {index === 0 ? '"' : ''}
31
+ {value.slice(start, end)}
32
+ {index === ranges.length - 1 ? '"' : ''}
33
+ </div>
34
+ );
35
+ }}
36
+ />
37
+ );
38
+ };
39
+
40
+ export const HexdumpValueViewer = ({ bytes }: { bytes: readonly number[] }) => {
41
+ const totalCount = Math.ceil(bytes.length / 16);
42
+
43
+ if (totalCount === 0) {
44
+ return <div className="text-xs text-muted italic">No bytes to display.</div>;
45
+ }
46
+
47
+ return (
48
+ <Virtuoso
49
+ aria-label="Buffer hexdump"
50
+ className="font-mono text-xs text-accent"
51
+ style={{ height: 384 }}
52
+ totalCount={totalCount}
53
+ itemContent={(index) => {
54
+ const row = formatHexdumpRow(bytes, index * 16);
55
+ return (
56
+ <div className="flex gap-4 leading-snug whitespace-pre">
57
+ <span className="text-muted">{row.offset}</span>
58
+ <span>{row.hex}</span>
59
+ <span className="text-muted">|{row.ascii}|</span>
60
+ </div>
61
+ );
62
+ }}
63
+ />
64
+ );
65
+ };