@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.
- package/CHANGELOG.md +46 -0
- package/README.md +6 -1
- package/dist/devtools/assets/panel-BlDRXgVW.js +32 -0
- package/dist/devtools/assets/panel-DBweZnW5.css +1 -0
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/async-storage.require.cjs +1 -1
- package/dist/react-native/chunks/async-storage.require.js +8 -7
- package/dist/react-native/chunks/index.require.cjs +1 -1
- package/dist/react-native/chunks/index.require.js +70 -66
- package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
- package/dist/react-native/chunks/secure-storage.require.js +17 -13
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -344
- package/dist/react-native/index.d.ts +166 -18
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.cjs +1 -1
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.js +21 -12
- package/package.json +35 -37
- package/react-native.ts +12 -22
- package/rozenite.config.ts +74 -0
- package/sdk.ts +1 -5
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +234 -0
- package/src/react-native/__tests__/export-snapshot.test.ts +138 -0
- package/src/react-native/__tests__/full-entry-request.test.ts +143 -0
- package/src/react-native/__tests__/import.test.ts +174 -11
- package/src/react-native/__tests__/storage-discovery.test.ts +144 -0
- package/src/react-native/__tests__/storage-view.test.ts +172 -0
- package/src/react-native/__tests__/stress-verification.test.ts +140 -0
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +221 -0
- package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
- package/src/react-native/adapters/async-storage.ts +8 -9
- package/src/react-native/adapters/index.ts +2 -8
- package/src/react-native/adapters/mmkv.ts +12 -24
- package/src/react-native/adapters/secure-storage.ts +4 -0
- package/src/react-native/entry-preview-pagination.ts +261 -0
- package/src/react-native/export-snapshot.ts +88 -0
- package/src/react-native/full-entry-request.ts +91 -0
- package/src/react-native/import.ts +179 -16
- package/src/react-native/storage-discovery.ts +92 -0
- package/src/react-native/storage-view.ts +47 -109
- package/src/react-native/useRozeniteStoragePlugin.ts +153 -129
- package/src/react-native/useStorageAgentTools.ts +196 -91
- package/src/shared/__tests__/entry-preview.test.ts +99 -0
- package/src/shared/__tests__/snapshot.test.ts +31 -32
- package/src/shared/agent-tools.ts +33 -38
- package/src/shared/entry-preview.ts +54 -0
- package/src/shared/messaging.ts +140 -21
- package/src/shared/snapshot.ts +12 -18
- package/src/shared/types.ts +21 -4
- package/src/ui/__tests__/binary-value-editor-state.test.ts +16 -13
- package/src/ui/__tests__/binary.test.ts +31 -33
- package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
- package/src/ui/__tests__/large-value-viewer.test.tsx +54 -0
- package/src/ui/__tests__/panel.test.tsx +382 -0
- package/src/ui/__tests__/storage-groups.test.ts +88 -0
- package/src/ui/__tests__/type-conversion.test.ts +8 -24
- package/src/ui/__tests__/use-storage-entries.test.tsx +337 -0
- package/src/ui/add-entry-dialog.tsx +66 -117
- package/src/ui/binary-value-editor-state.ts +16 -25
- package/src/ui/binary-value-editor.tsx +59 -47
- package/src/ui/binary.ts +54 -15
- package/src/ui/edit-entry-dialog.tsx +68 -104
- package/src/ui/editor-switcher.tsx +9 -16
- package/src/ui/entry-detail-dialog.tsx +62 -181
- package/src/ui/format-value.tsx +25 -0
- package/src/ui/globals.css +1 -123
- package/src/ui/import-dialog.tsx +72 -109
- package/src/ui/large-value-viewer-state.ts +25 -0
- package/src/ui/large-value-viewer.tsx +65 -0
- package/src/ui/panel.tsx +649 -509
- package/src/ui/query-client.tsx +23 -0
- package/src/ui/storage-groups.ts +66 -0
- package/src/ui/type-conversion.ts +2 -5
- package/src/ui/typed-value-editor.tsx +15 -20
- package/src/ui/use-storage-entries.ts +192 -0
- package/src/ui/utils.ts +1 -4
- package/tsconfig.json +5 -1
- package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
- package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
- package/postcss.config.js +0 -6
- package/src/ui/confirm-dialog.tsx +0 -100
- package/src/ui/editable-table.tsx +0 -300
- package/tailwind.config.ts +0 -94
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
base64ToBytes,
|
|
3
|
-
bytesToBase64,
|
|
4
|
-
bytesToGroupedHex,
|
|
5
|
-
hexInputToBytes,
|
|
6
|
-
} from './binary';
|
|
1
|
+
import { base64ToBytes, bytesToBase64, bytesToGroupedHex, hexInputToBytes } from './binary';
|
|
7
2
|
|
|
8
3
|
export type EditorMode = 'hex' | 'base64';
|
|
9
4
|
|
|
@@ -17,11 +12,10 @@ export type EditorState = {
|
|
|
17
12
|
export type EditorAction =
|
|
18
13
|
| { type: 'set-text'; text: string }
|
|
19
14
|
| { type: 'normalize-paste'; text: string }
|
|
20
|
-
| { type: 'switch-mode'; mode: EditorMode }
|
|
15
|
+
| { type: 'switch-mode'; mode: EditorMode }
|
|
16
|
+
| { type: 'replace-bytes'; bytes: number[] };
|
|
21
17
|
|
|
22
|
-
export type Validation =
|
|
23
|
-
| { ok: true; bytes: number[] }
|
|
24
|
-
| { ok: false; reason: string };
|
|
18
|
+
export type Validation = { ok: true; bytes: number[] } | { ok: false; reason: string };
|
|
25
19
|
|
|
26
20
|
const encode = (bytes: readonly number[], mode: EditorMode): string =>
|
|
27
21
|
mode === 'hex' ? bytesToGroupedHex(bytes) : bytesToBase64(bytes);
|
|
@@ -29,20 +23,12 @@ const encode = (bytes: readonly number[], mode: EditorMode): string =>
|
|
|
29
23
|
const parse = (text: string, mode: EditorMode) =>
|
|
30
24
|
mode === 'hex' ? hexInputToBytes(text) : base64ToBytes(text);
|
|
31
25
|
|
|
32
|
-
const parsedToState = (
|
|
33
|
-
text: string,
|
|
34
|
-
mode: EditorMode,
|
|
35
|
-
): Pick<EditorState, 'bytes' | 'error'> => {
|
|
26
|
+
const parsedToState = (text: string, mode: EditorMode): Pick<EditorState, 'bytes' | 'error'> => {
|
|
36
27
|
const result = parse(text, mode);
|
|
37
|
-
return result.ok
|
|
38
|
-
? { bytes: result.value, error: null }
|
|
39
|
-
: { bytes: null, error: result.error };
|
|
28
|
+
return result.ok ? { bytes: result.value, error: null } : { bytes: null, error: result.error };
|
|
40
29
|
};
|
|
41
30
|
|
|
42
|
-
export const initialState = (args: {
|
|
43
|
-
initialBytes?: number[];
|
|
44
|
-
mode?: EditorMode;
|
|
45
|
-
}): EditorState => {
|
|
31
|
+
export const initialState = (args: { initialBytes?: number[]; mode?: EditorMode }): EditorState => {
|
|
46
32
|
const mode = args.mode ?? 'hex';
|
|
47
33
|
if (args.initialBytes && args.initialBytes.length > 0) {
|
|
48
34
|
return {
|
|
@@ -60,10 +46,7 @@ export const initialState = (args: {
|
|
|
60
46
|
};
|
|
61
47
|
};
|
|
62
48
|
|
|
63
|
-
export const reduce = (
|
|
64
|
-
state: EditorState,
|
|
65
|
-
action: EditorAction,
|
|
66
|
-
): EditorState => {
|
|
49
|
+
export const reduce = (state: EditorState, action: EditorAction): EditorState => {
|
|
67
50
|
switch (action.type) {
|
|
68
51
|
case 'set-text': {
|
|
69
52
|
return {
|
|
@@ -108,6 +91,14 @@ export const reduce = (
|
|
|
108
91
|
error: null,
|
|
109
92
|
};
|
|
110
93
|
}
|
|
94
|
+
case 'replace-bytes': {
|
|
95
|
+
return {
|
|
96
|
+
mode: state.mode,
|
|
97
|
+
text: encode(action.bytes, state.mode),
|
|
98
|
+
bytes: action.bytes,
|
|
99
|
+
error: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
111
102
|
}
|
|
112
103
|
};
|
|
113
104
|
|
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
|
2
2
|
import { EditorState as CMEditorState } from '@codemirror/state';
|
|
3
3
|
import { EditorView, keymap } from '@codemirror/view';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
reduce,
|
|
9
|
-
type EditorMode,
|
|
10
|
-
} from './binary-value-editor-state';
|
|
4
|
+
import { cn } from '@rozenite/ui';
|
|
5
|
+
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
6
|
+
import { compactAsciiPreview } from './binary';
|
|
7
|
+
import { initialState, reduce, type EditorMode } from './binary-value-editor-state';
|
|
11
8
|
|
|
12
9
|
export type BinaryValueEditorProps = {
|
|
13
10
|
initialBytes?: number[];
|
|
14
11
|
onChange: (bytes: number[] | null) => void;
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
caretColor: '#60a5fa',
|
|
26
|
-
fontFamily:
|
|
27
|
-
'ui-monospace, "Cascadia Mono", "Fira Code", Menlo, monospace',
|
|
28
|
-
padding: '8px',
|
|
29
|
-
},
|
|
30
|
-
'.cm-focused': { outline: 'none' },
|
|
31
|
-
'.cm-gutters': { display: 'none' },
|
|
32
|
-
'.cm-scroller': { overflow: 'auto' },
|
|
14
|
+
// Pulls colors from the shared design tokens (set by `PluginShell` on its
|
|
15
|
+
// root as CSS custom properties) so the editor follows the active
|
|
16
|
+
// light/dark theme instead of being locked to one palette.
|
|
17
|
+
const editorTheme = EditorView.theme({
|
|
18
|
+
'&': {
|
|
19
|
+
color: 'var(--foreground)',
|
|
20
|
+
backgroundColor: 'transparent',
|
|
21
|
+
fontSize: '12px',
|
|
33
22
|
},
|
|
34
|
-
|
|
35
|
-
)
|
|
23
|
+
'.cm-content': {
|
|
24
|
+
caretColor: 'var(--ring)',
|
|
25
|
+
fontFamily: 'ui-monospace, "Cascadia Mono", "Fira Code", Menlo, monospace',
|
|
26
|
+
padding: '8px',
|
|
27
|
+
},
|
|
28
|
+
'.cm-focused': { outline: 'none' },
|
|
29
|
+
'.cm-gutters': { display: 'none' },
|
|
30
|
+
'.cm-scroller': { overflow: 'auto' },
|
|
31
|
+
});
|
|
36
32
|
|
|
37
33
|
const ModeButton = ({
|
|
38
34
|
label,
|
|
@@ -46,22 +42,22 @@ const ModeButton = ({
|
|
|
46
42
|
<button
|
|
47
43
|
type="button"
|
|
48
44
|
onClick={onClick}
|
|
49
|
-
className={
|
|
45
|
+
className={cn(
|
|
46
|
+
'rounded-md px-2 py-1 text-xs font-medium transition-colors',
|
|
50
47
|
active
|
|
51
|
-
? 'bg-
|
|
52
|
-
: '
|
|
53
|
-
}
|
|
48
|
+
? 'bg-primary text-primary-foreground'
|
|
49
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
50
|
+
)}
|
|
54
51
|
>
|
|
55
52
|
{label}
|
|
56
53
|
</button>
|
|
57
54
|
);
|
|
58
55
|
|
|
59
|
-
export const BinaryValueEditor = ({
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
initialState({ initialBytes }),
|
|
56
|
+
export const BinaryValueEditor = ({ initialBytes, onChange }: BinaryValueEditorProps) => {
|
|
57
|
+
const [state, dispatch] = useReducer(reduce, undefined, () => initialState({}));
|
|
58
|
+
const initialBytesRef = useRef(initialBytes);
|
|
59
|
+
const [isPreparing, setIsPreparing] = useState(
|
|
60
|
+
Boolean(initialBytesRef.current && initialBytesRef.current.length > 0),
|
|
65
61
|
);
|
|
66
62
|
|
|
67
63
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -71,6 +67,19 @@ export const BinaryValueEditor = ({
|
|
|
71
67
|
const onChangeRef = useRef(onChange);
|
|
72
68
|
onChangeRef.current = onChange;
|
|
73
69
|
|
|
70
|
+
// Formatting a complete binary editor document is unavoidable when editing,
|
|
71
|
+
// but not when the dialog first opens. Yield once so the dialog can paint a
|
|
72
|
+
// size-aware loading state before constructing a potentially huge document.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const initialBytes = initialBytesRef.current;
|
|
75
|
+
if (!initialBytes?.length) return;
|
|
76
|
+
const timeout = window.setTimeout(() => {
|
|
77
|
+
dispatch({ type: 'replace-bytes', bytes: initialBytes });
|
|
78
|
+
setIsPreparing(false);
|
|
79
|
+
}, 0);
|
|
80
|
+
return () => window.clearTimeout(timeout);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
74
83
|
// Mount the CodeMirror view once. The update listener compares the
|
|
75
84
|
// doc to the latest reducer text (via stateRef) so paste-or-type
|
|
76
85
|
// events round-trip through the reducer instead of looping back.
|
|
@@ -84,15 +93,13 @@ export const BinaryValueEditor = ({
|
|
|
84
93
|
extensions: [
|
|
85
94
|
history(),
|
|
86
95
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
87
|
-
|
|
96
|
+
editorTheme,
|
|
88
97
|
EditorView.lineWrapping,
|
|
89
98
|
EditorView.updateListener.of((update) => {
|
|
90
99
|
if (!update.docChanged) return;
|
|
91
100
|
const newText = update.state.doc.toString();
|
|
92
101
|
if (newText === stateRef.current.text) return;
|
|
93
|
-
const isPaste = update.transactions.some((tr) =>
|
|
94
|
-
tr.isUserEvent('input.paste'),
|
|
95
|
-
);
|
|
102
|
+
const isPaste = update.transactions.some((tr) => tr.isUserEvent('input.paste'));
|
|
96
103
|
dispatch({
|
|
97
104
|
type: isPaste ? 'normalize-paste' : 'set-text',
|
|
98
105
|
text: newText,
|
|
@@ -125,8 +132,8 @@ export const BinaryValueEditor = ({
|
|
|
125
132
|
onChangeRef.current(state.bytes);
|
|
126
133
|
}, [state.bytes]);
|
|
127
134
|
|
|
128
|
-
const byteCount = state.bytes?.length ?? 0;
|
|
129
|
-
const asciiPreview = state.bytes ?
|
|
135
|
+
const byteCount = state.bytes?.length ?? initialBytes?.length ?? 0;
|
|
136
|
+
const asciiPreview = state.bytes ? compactAsciiPreview(state.bytes) : '';
|
|
130
137
|
|
|
131
138
|
const handleModeChange = (mode: EditorMode) => {
|
|
132
139
|
if (state.mode === mode) return;
|
|
@@ -150,18 +157,23 @@ export const BinaryValueEditor = ({
|
|
|
150
157
|
|
|
151
158
|
<div
|
|
152
159
|
ref={hostRef}
|
|
153
|
-
className="min-h-[120px] max-h-[300px] overflow-auto rounded border border-
|
|
160
|
+
className="min-h-[120px] max-h-[300px] overflow-auto rounded-md border border-input bg-muted"
|
|
154
161
|
/>
|
|
155
162
|
|
|
163
|
+
{isPreparing ? (
|
|
164
|
+
<div className="text-xs text-muted-foreground">
|
|
165
|
+
Preparing editor for {initialBytes?.length ?? 0} bytes…
|
|
166
|
+
</div>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
156
169
|
<div className="flex flex-col gap-1 text-xs">
|
|
157
|
-
<div className="text-
|
|
170
|
+
<div className="text-muted-foreground">{byteCount} bytes</div>
|
|
158
171
|
{asciiPreview && (
|
|
159
|
-
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-
|
|
160
|
-
ASCII:{
|
|
161
|
-
<span className="font-mono text-gray-300">{asciiPreview}</span>
|
|
172
|
+
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
|
173
|
+
ASCII: <span className="font-mono text-foreground">{asciiPreview}</span>
|
|
162
174
|
</div>
|
|
163
175
|
)}
|
|
164
|
-
{state.error && <div className="text-
|
|
176
|
+
{state.error && <div className="text-destructive">{state.error}</div>}
|
|
165
177
|
</div>
|
|
166
178
|
</div>
|
|
167
179
|
);
|
package/src/ui/binary.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export type ParseResult<T> =
|
|
2
|
-
| { ok: true; value: T }
|
|
3
|
-
| { ok: false; error: string };
|
|
1
|
+
export type ParseResult<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
4
2
|
|
|
5
3
|
const BYTES_PER_LINE = 16;
|
|
6
4
|
const BYTES_PER_GROUP = 8;
|
|
@@ -11,13 +9,10 @@ const OFFSET_WIDTH = 8;
|
|
|
11
9
|
const ASCII_PRINTABLE_MIN = 0x20;
|
|
12
10
|
const ASCII_PRINTABLE_MAX = 0x7e;
|
|
13
11
|
|
|
14
|
-
const toHexPair = (byte: number): string =>
|
|
15
|
-
byte.toString(16).toUpperCase().padStart(2, '0');
|
|
12
|
+
const toHexPair = (byte: number): string => byte.toString(16).toUpperCase().padStart(2, '0');
|
|
16
13
|
|
|
17
14
|
const toAsciiChar = (byte: number): string =>
|
|
18
|
-
byte >= ASCII_PRINTABLE_MIN && byte <= ASCII_PRINTABLE_MAX
|
|
19
|
-
? String.fromCharCode(byte)
|
|
20
|
-
: '.';
|
|
15
|
+
byte >= ASCII_PRINTABLE_MIN && byte <= ASCII_PRINTABLE_MAX ? String.fromCharCode(byte) : '.';
|
|
21
16
|
|
|
22
17
|
const formatHexLine = (slice: readonly number[]): string => {
|
|
23
18
|
const left = slice.slice(0, BYTES_PER_GROUP).map(toHexPair).join(' ');
|
|
@@ -25,6 +20,37 @@ const formatHexLine = (slice: readonly number[]): string => {
|
|
|
25
20
|
return right ? `${left} ${right}` : left;
|
|
26
21
|
};
|
|
27
22
|
|
|
23
|
+
export type HexdumpRow = {
|
|
24
|
+
offset: string;
|
|
25
|
+
hex: string;
|
|
26
|
+
ascii: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Produces one visible hexdump row. Unlike bytesToHexdump this never builds a
|
|
30
|
+
// representation for bytes outside the requested row.
|
|
31
|
+
export const formatHexdumpRow = (bytes: readonly number[], rowStart: number): HexdumpRow => {
|
|
32
|
+
let left = '';
|
|
33
|
+
let right = '';
|
|
34
|
+
let ascii = '';
|
|
35
|
+
const rowEnd = Math.min(rowStart + BYTES_PER_LINE, bytes.length);
|
|
36
|
+
|
|
37
|
+
for (let index = rowStart; index < rowEnd; index++) {
|
|
38
|
+
const hex = toHexPair(bytes[index]);
|
|
39
|
+
if (index < rowStart + BYTES_PER_GROUP) {
|
|
40
|
+
left += left ? ` ${hex}` : hex;
|
|
41
|
+
} else {
|
|
42
|
+
right += right ? ` ${hex}` : hex;
|
|
43
|
+
}
|
|
44
|
+
ascii += toAsciiChar(bytes[index]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
offset: rowStart.toString(16).padStart(OFFSET_WIDTH, '0'),
|
|
49
|
+
hex: right ? `${left} ${right}` : left,
|
|
50
|
+
ascii,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
28
54
|
export const bytesToGroupedHex = (bytes: readonly number[]): string => {
|
|
29
55
|
const lines: string[] = [];
|
|
30
56
|
for (let i = 0; i < bytes.length; i += BYTES_PER_LINE) {
|
|
@@ -48,12 +74,27 @@ export const bytesToHexdump = (bytes: readonly number[]): string => {
|
|
|
48
74
|
export const bytesToAsciiPreview = (bytes: readonly number[]): string =>
|
|
49
75
|
bytes.map(toAsciiChar).join('');
|
|
50
76
|
|
|
77
|
+
export const compactAsciiPreview = (bytes: readonly number[], maxBytes = 64): string => {
|
|
78
|
+
let preview = '';
|
|
79
|
+
const limit = Math.min(bytes.length, maxBytes);
|
|
80
|
+
for (let index = 0; index < limit; index++) {
|
|
81
|
+
preview += toAsciiChar(bytes[index]);
|
|
82
|
+
}
|
|
83
|
+
return bytes.length > maxBytes ? `${preview}…` : preview;
|
|
84
|
+
};
|
|
85
|
+
|
|
51
86
|
export const bytesToBase64 = (bytes: readonly number[]): string => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
const chunks: string[] = [];
|
|
88
|
+
const chunkSize = 8_192;
|
|
89
|
+
for (let start = 0; start < bytes.length; start += chunkSize) {
|
|
90
|
+
let chunk = '';
|
|
91
|
+
const end = Math.min(start + chunkSize, bytes.length);
|
|
92
|
+
for (let index = start; index < end; index++) {
|
|
93
|
+
chunk += String.fromCharCode(bytes[index]);
|
|
94
|
+
}
|
|
95
|
+
chunks.push(chunk);
|
|
55
96
|
}
|
|
56
|
-
return btoa(
|
|
97
|
+
return btoa(chunks.join(''));
|
|
57
98
|
};
|
|
58
99
|
|
|
59
100
|
export const compactBufferPreview = (
|
|
@@ -81,9 +122,7 @@ const TRAILING_ASCII_COLUMN_RE = /\|[^|]*\|\s*$/;
|
|
|
81
122
|
export const hexInputToBytes = (input: string): ParseResult<number[]> => {
|
|
82
123
|
const cleaned = input
|
|
83
124
|
.split(/\r?\n/)
|
|
84
|
-
.map((line) =>
|
|
85
|
-
line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''),
|
|
86
|
-
)
|
|
125
|
+
.map((line) => line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''))
|
|
87
126
|
.join('')
|
|
88
127
|
.replace(/0x/gi, '')
|
|
89
128
|
.replace(/\s+/g, '');
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
StorageEntry,
|
|
5
|
-
StorageEntryType,
|
|
6
|
-
StorageEntryValue,
|
|
7
|
-
} from '../shared/types';
|
|
8
|
-
import { ConfirmDialog } from './confirm-dialog';
|
|
1
|
+
import { Button, ConfirmDialog, Dialog, Field } from '@rozenite/ui';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import type { StorageEntry, StorageEntryType, StorageEntryValue } from '../shared/types';
|
|
9
4
|
import { TypedValueEditor } from './typed-value-editor';
|
|
10
5
|
import { defaultValueForType } from './type-conversion';
|
|
11
6
|
|
|
@@ -28,23 +23,23 @@ export const EditEntryDialog = ({
|
|
|
28
23
|
entry,
|
|
29
24
|
supportedTypes,
|
|
30
25
|
}: EditEntryDialogProps) => {
|
|
26
|
+
const lastEntryRef = useRef<StorageEntry | null>(entry);
|
|
31
27
|
const [currentType, setCurrentType] = useState<StorageEntryType>('string');
|
|
32
28
|
const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
|
|
33
29
|
defaultValueForType('string'),
|
|
34
30
|
);
|
|
35
|
-
const [
|
|
36
|
-
isOpen: boolean;
|
|
37
|
-
title: string;
|
|
38
|
-
message: string;
|
|
39
|
-
}>({ isOpen: false, title: '', message: '' });
|
|
31
|
+
const [alert, setAlert] = useState<{ title: string; message: string } | null>(null);
|
|
40
32
|
|
|
41
33
|
useEffect(() => {
|
|
42
34
|
if (entry && isOpen) {
|
|
35
|
+
lastEntryRef.current = entry;
|
|
43
36
|
setCurrentType(entry.type);
|
|
44
37
|
setCurrentValue(entry.value);
|
|
45
38
|
}
|
|
46
39
|
}, [entry, isOpen]);
|
|
47
40
|
|
|
41
|
+
const entryForDisplay = entry ?? lastEntryRef.current;
|
|
42
|
+
|
|
48
43
|
const isCurrentTypeSupported = supportedTypes.includes(currentType);
|
|
49
44
|
|
|
50
45
|
const resetAndClose = () => {
|
|
@@ -57,8 +52,7 @@ export const EditEntryDialog = ({
|
|
|
57
52
|
if (!entry || currentValue === null) return;
|
|
58
53
|
|
|
59
54
|
if (!isCurrentTypeSupported) {
|
|
60
|
-
|
|
61
|
-
isOpen: true,
|
|
55
|
+
setAlert({
|
|
62
56
|
title: 'Unsupported Type',
|
|
63
57
|
message: 'This storage does not support the selected type.',
|
|
64
58
|
});
|
|
@@ -70,16 +64,12 @@ export const EditEntryDialog = ({
|
|
|
70
64
|
};
|
|
71
65
|
|
|
72
66
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
73
|
-
if (event.key === 'Escape') {
|
|
74
|
-
resetAndClose();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
67
|
if (event.key === 'Enter' && currentType !== 'buffer') {
|
|
78
68
|
handleSave();
|
|
79
69
|
}
|
|
80
70
|
};
|
|
81
71
|
|
|
82
|
-
if (!
|
|
72
|
+
if (!entryForDisplay) {
|
|
83
73
|
return null;
|
|
84
74
|
}
|
|
85
75
|
|
|
@@ -89,95 +79,69 @@ export const EditEntryDialog = ({
|
|
|
89
79
|
const isSaveDisabled = !isCurrentTypeSupported || currentValue === null;
|
|
90
80
|
|
|
91
81
|
return (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
onClick={(event) => event.stopPropagation()}
|
|
99
|
-
onKeyDown={handleKeyDown}
|
|
82
|
+
<>
|
|
83
|
+
<Dialog
|
|
84
|
+
open={isOpen}
|
|
85
|
+
onOpenChange={(open) => {
|
|
86
|
+
if (!open) resetAndClose();
|
|
87
|
+
}}
|
|
100
88
|
>
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
89
|
+
<Dialog.Content onKeyDown={handleKeyDown}>
|
|
90
|
+
<Dialog.Header>
|
|
91
|
+
<Dialog.Title>Edit Entry</Dialog.Title>
|
|
92
|
+
</Dialog.Header>
|
|
93
|
+
|
|
94
|
+
<div className="flex flex-col gap-4">
|
|
95
|
+
<Field>
|
|
96
|
+
<Field.Label>Key</Field.Label>
|
|
97
|
+
<div className="h-8 w-full truncate rounded-md border border-input bg-muted px-3 py-1.5 font-mono text-sm text-foreground">
|
|
98
|
+
{entryForDisplay.key}
|
|
99
|
+
</div>
|
|
100
|
+
<Field.Description>Key cannot be changed during editing</Field.Description>
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
<Field>
|
|
104
|
+
<Field.Label htmlFor="edit-entry-value">Value</Field.Label>
|
|
105
|
+
<TypedValueEditor
|
|
106
|
+
key={entryForDisplay.key}
|
|
107
|
+
supportedTypes={supportedTypes}
|
|
108
|
+
type={currentType}
|
|
109
|
+
value={currentValue}
|
|
110
|
+
onChange={(nextType, nextValue) => {
|
|
111
|
+
setCurrentType(nextType);
|
|
112
|
+
setCurrentValue(nextValue);
|
|
113
|
+
}}
|
|
114
|
+
inputId="edit-entry-value"
|
|
115
|
+
autoFocus
|
|
116
|
+
/>
|
|
117
|
+
{!isCurrentTypeSupported && (
|
|
118
|
+
<Field.Description className="text-destructive">
|
|
119
|
+
This storage does not support {currentType} values.
|
|
120
|
+
</Field.Description>
|
|
121
|
+
)}
|
|
122
|
+
</Field>
|
|
126
123
|
</div>
|
|
127
124
|
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
>
|
|
133
|
-
|
|
134
|
-
</
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
value={currentValue}
|
|
139
|
-
onChange={(nextType, nextValue) => {
|
|
140
|
-
setCurrentType(nextType);
|
|
141
|
-
setCurrentValue(nextValue);
|
|
142
|
-
}}
|
|
143
|
-
inputId="edit-entry-value"
|
|
144
|
-
autoFocus
|
|
145
|
-
/>
|
|
146
|
-
{!isCurrentTypeSupported && (
|
|
147
|
-
<p className="text-xs text-amber-400 mt-1">
|
|
148
|
-
This storage does not support {currentType} values.
|
|
149
|
-
</p>
|
|
150
|
-
)}
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<div className="flex items-center justify-end gap-2 mt-6">
|
|
155
|
-
<button
|
|
156
|
-
onClick={resetAndClose}
|
|
157
|
-
className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
|
|
158
|
-
>
|
|
159
|
-
Cancel
|
|
160
|
-
</button>
|
|
161
|
-
<button
|
|
162
|
-
onClick={handleSave}
|
|
163
|
-
disabled={isSaveDisabled}
|
|
164
|
-
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"
|
|
165
|
-
>
|
|
166
|
-
Save Changes
|
|
167
|
-
</button>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
125
|
+
<Dialog.Footer>
|
|
126
|
+
<Button variant="outline" onClick={resetAndClose}>
|
|
127
|
+
Cancel
|
|
128
|
+
</Button>
|
|
129
|
+
<Button onClick={handleSave} disabled={isSaveDisabled}>
|
|
130
|
+
Save Changes
|
|
131
|
+
</Button>
|
|
132
|
+
</Dialog.Footer>
|
|
133
|
+
</Dialog.Content>
|
|
134
|
+
</Dialog>
|
|
170
135
|
|
|
171
136
|
<ConfirmDialog
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
title={
|
|
178
|
-
|
|
179
|
-
type="alert"
|
|
137
|
+
open={alert !== null}
|
|
138
|
+
onOpenChange={(open) => {
|
|
139
|
+
if (!open) setAlert(null);
|
|
140
|
+
}}
|
|
141
|
+
variant="alert"
|
|
142
|
+
title={alert?.title ?? ''}
|
|
143
|
+
description={alert?.message}
|
|
180
144
|
/>
|
|
181
|
-
|
|
145
|
+
</>
|
|
182
146
|
);
|
|
183
147
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cn } from '@rozenite/ui';
|
|
1
2
|
import type { StorageEntryType } from '../shared/types';
|
|
2
3
|
|
|
3
4
|
const TYPE_LABELS: Record<StorageEntryType, string> = {
|
|
@@ -9,12 +10,7 @@ const TYPE_LABELS: Record<StorageEntryType, string> = {
|
|
|
9
10
|
|
|
10
11
|
// Visual ordering of the pills. Keep `string` first because it's the
|
|
11
12
|
// most common landing type after a fresh read on an MMKV key.
|
|
12
|
-
const TYPE_ORDER: StorageEntryType[] = [
|
|
13
|
-
'string',
|
|
14
|
-
'number',
|
|
15
|
-
'boolean',
|
|
16
|
-
'buffer',
|
|
17
|
-
];
|
|
13
|
+
const TYPE_ORDER: StorageEntryType[] = ['string', 'number', 'boolean', 'buffer'];
|
|
18
14
|
|
|
19
15
|
export type EditorSwitcherProps = {
|
|
20
16
|
supportedTypes: StorageEntryType[];
|
|
@@ -22,11 +18,7 @@ export type EditorSwitcherProps = {
|
|
|
22
18
|
onChange: (type: StorageEntryType) => void;
|
|
23
19
|
};
|
|
24
20
|
|
|
25
|
-
export const EditorSwitcher = ({
|
|
26
|
-
supportedTypes,
|
|
27
|
-
value,
|
|
28
|
-
onChange,
|
|
29
|
-
}: EditorSwitcherProps) => {
|
|
21
|
+
export const EditorSwitcher = ({ supportedTypes, value, onChange }: EditorSwitcherProps) => {
|
|
30
22
|
const available = TYPE_ORDER.filter((type) => supportedTypes.includes(type));
|
|
31
23
|
|
|
32
24
|
// Adaptive hide: the switcher is meaningless when the backend only
|
|
@@ -39,7 +31,7 @@ export const EditorSwitcher = ({
|
|
|
39
31
|
<div
|
|
40
32
|
role="tablist"
|
|
41
33
|
aria-label="Edit as"
|
|
42
|
-
className="inline-flex rounded border border-
|
|
34
|
+
className="inline-flex overflow-hidden rounded-md border border-border"
|
|
43
35
|
>
|
|
44
36
|
{available.map((type) => (
|
|
45
37
|
<button
|
|
@@ -48,11 +40,12 @@ export const EditorSwitcher = ({
|
|
|
48
40
|
role="tab"
|
|
49
41
|
aria-selected={value === type}
|
|
50
42
|
onClick={() => onChange(type)}
|
|
51
|
-
className={
|
|
43
|
+
className={cn(
|
|
44
|
+
'px-3 py-1 text-xs font-medium transition-colors',
|
|
52
45
|
value === type
|
|
53
|
-
? 'bg-
|
|
54
|
-
: 'text-
|
|
55
|
-
}
|
|
46
|
+
? 'bg-primary text-primary-foreground'
|
|
47
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
48
|
+
)}
|
|
56
49
|
>
|
|
57
50
|
{TYPE_LABELS[type]}
|
|
58
51
|
</button>
|