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