@rozenite/storage-plugin 2.0.0 → 2.2.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 (69) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/devtools/assets/panel-D5z1BY8U.js +32 -0
  3. package/dist/devtools/assets/panel-DVlWRg6d.css +1 -0
  4. package/dist/devtools/panel.html +2 -2
  5. package/dist/react-native/chunks/index.require.js +1 -3
  6. package/dist/react-native/chunks/secure-storage.require.js +2 -6
  7. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  8. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
  9. package/dist/react-native/index.d.ts +14 -2
  10. package/dist/react-native/index.js +2 -10
  11. package/dist/rozenite.json +1 -1
  12. package/package.json +29 -29
  13. package/react-native.ts +11 -36
  14. package/rozenite.config.ts +2 -6
  15. package/sdk.ts +1 -5
  16. package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
  17. package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
  18. package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
  19. package/src/react-native/__tests__/import.test.ts +9 -8
  20. package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
  21. package/src/react-native/__tests__/stress-verification.test.ts +5 -18
  22. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
  23. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  24. package/src/react-native/adapters/async-storage.ts +2 -5
  25. package/src/react-native/adapters/index.ts +2 -8
  26. package/src/react-native/adapters/mmkv.ts +7 -24
  27. package/src/react-native/adapters/secure-storage.ts +2 -6
  28. package/src/react-native/entry-preview-pagination.ts +11 -27
  29. package/src/react-native/export-snapshot.ts +3 -9
  30. package/src/react-native/full-entry-request.ts +3 -8
  31. package/src/react-native/import.ts +10 -21
  32. package/src/react-native/storage-discovery.ts +2 -6
  33. package/src/react-native/storage-view.ts +2 -6
  34. package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
  35. package/src/react-native/useStorageAgentTools.ts +8 -24
  36. package/src/shared/__tests__/entry-preview.test.ts +9 -27
  37. package/src/shared/__tests__/snapshot.test.ts +10 -32
  38. package/src/shared/agent-tools.ts +9 -30
  39. package/src/shared/entry-preview.ts +3 -8
  40. package/src/shared/messaging.ts +15 -11
  41. package/src/shared/snapshot.ts +4 -17
  42. package/src/shared/types.ts +2 -4
  43. package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
  44. package/src/ui/__tests__/binary.test.ts +13 -33
  45. package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
  46. package/src/ui/__tests__/panel.test.tsx +50 -63
  47. package/src/ui/__tests__/storage-groups.test.ts +1 -4
  48. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  49. package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
  50. package/src/ui/add-entry-dialog.tsx +66 -87
  51. package/src/ui/binary-value-editor-state.ts +6 -24
  52. package/src/ui/binary-value-editor.tsx +6 -18
  53. package/src/ui/binary.ts +6 -19
  54. package/src/ui/edit-entry-dialog.tsx +59 -78
  55. package/src/ui/editor-switcher.tsx +2 -11
  56. package/src/ui/entry-detail-dialog.tsx +9 -16
  57. package/src/ui/format-value.tsx +2 -9
  58. package/src/ui/import-dialog.tsx +12 -24
  59. package/src/ui/large-value-viewer-state.ts +2 -9
  60. package/src/ui/large-value-viewer.tsx +3 -9
  61. package/src/ui/panel.tsx +129 -235
  62. package/src/ui/query-client.tsx +4 -15
  63. package/src/ui/type-conversion.ts +2 -5
  64. package/src/ui/typed-value-editor.tsx +1 -5
  65. package/src/ui/use-storage-entries.ts +6 -21
  66. package/src/ui/utils.ts +1 -4
  67. package/tsconfig.json +4 -7
  68. package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
  69. package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
@@ -97,11 +97,8 @@ describe('storage entry query hooks', () => {
97
97
  if (payload.cursor === undefined)
98
98
  return Promise.resolve(response(undefined, { next: 'next' }));
99
99
  if (payload.cursor === 'next')
100
- return Promise.resolve(
101
- response('next', { next: 'last', previous: 'first' }),
102
- );
103
- if (payload.cursor === 'last')
104
- return Promise.resolve(response('last', { previous: 'next' }));
100
+ return Promise.resolve(response('next', { next: 'last', previous: 'first' }));
101
+ if (payload.cursor === 'last') return Promise.resolve(response('last', { previous: 'next' }));
105
102
  return Promise.resolve(response('first', { next: 'next' }));
106
103
  });
107
104
  const hook = await renderPreviewHook({
@@ -117,9 +114,11 @@ describe('storage entry query hooks', () => {
117
114
  });
118
115
  await act(async () => hook.result.fetchNextPage());
119
116
  await act(async () => hook.result.fetchNextPage());
120
- expect(
121
- request.mock.calls.map(([options]) => options.payload.cursor),
122
- ).toEqual([undefined, 'next', 'last']);
117
+ expect(request.mock.calls.map(([options]) => options.payload.cursor)).toEqual([
118
+ undefined,
119
+ 'next',
120
+ 'last',
121
+ ]);
123
122
  const queryKey = storageEntryPreviewQueryKey(target, '', 'ascending');
124
123
  const pageKeys = () =>
125
124
  hook.queryClient
@@ -134,9 +133,7 @@ describe('storage entry query hooks', () => {
134
133
 
135
134
  it('cancels stale target requests and starts a new sequence at the first page', async () => {
136
135
  globalThis.IS_REACT_ACT_ENVIRONMENT = true;
137
- let resolveFirst:
138
- | ((value: ReturnType<typeof response>) => void)
139
- | undefined;
136
+ let resolveFirst: ((value: ReturnType<typeof response>) => void) | undefined;
140
137
  const request = vi.fn(({ payload }) => {
141
138
  if (payload.target === target) {
142
139
  return new Promise((resolve) => {
@@ -171,8 +168,7 @@ describe('storage entry query hooks', () => {
171
168
  expect(
172
169
  hook.queryClient.getQueryData<{
173
170
  pages: Array<ReturnType<typeof response>>;
174
- }>(storageEntryPreviewQueryKey(otherTarget, 'two', 'descending'))
175
- ?.pages[0].target,
171
+ }>(storageEntryPreviewQueryKey(otherTarget, 'two', 'descending'))?.pages[0].target,
176
172
  ).toEqual(otherTarget),
177
173
  );
178
174
  await hook.unmount();
@@ -243,9 +239,7 @@ describe('storage entry query hooks', () => {
243
239
 
244
240
  it('resets every selected-storage preview variant but refetches only the active first page', async () => {
245
241
  globalThis.IS_REACT_ACT_ENVIRONMENT = true;
246
- const request = vi.fn(({ payload }) =>
247
- Promise.resolve(response(payload.cursor)),
248
- );
242
+ const request = vi.fn(({ payload }) => Promise.resolve(response(payload.cursor)));
249
243
  const hook = await renderPreviewHook({
250
244
  client: asClient(request),
251
245
  target,
@@ -255,28 +249,22 @@ describe('storage entry query hooks', () => {
255
249
  await act(async () => {
256
250
  await vi.waitFor(() => expect(hook.result.isSuccess).toBe(true));
257
251
  });
258
- hook.queryClient.setQueryData(
259
- storageEntryPreviewQueryKey(target, 'inactive', 'ascending'),
260
- { pages: [response(undefined)], pageParams: [undefined] },
261
- );
252
+ hook.queryClient.setQueryData(storageEntryPreviewQueryKey(target, 'inactive', 'ascending'), {
253
+ pages: [response(undefined)],
254
+ pageParams: [undefined],
255
+ });
262
256
 
263
- await act(async () =>
264
- resetSelectedStorageQueries(hook.queryClient, target),
265
- );
257
+ await act(async () => resetSelectedStorageQueries(hook.queryClient, target));
266
258
  expect(request).toHaveBeenCalledTimes(2);
267
259
  expect(
268
- hook.queryClient.getQueryData(
269
- storageEntryPreviewQueryKey(target, 'inactive', 'ascending'),
270
- ),
260
+ hook.queryClient.getQueryData(storageEntryPreviewQueryKey(target, 'inactive', 'ascending')),
271
261
  ).toBeUndefined();
272
262
  await hook.unmount();
273
263
  });
274
264
 
275
265
  it('invalidates only the changed storage preview prefix and drops its stale full value', async () => {
276
266
  globalThis.IS_REACT_ACT_ENVIRONMENT = true;
277
- const request = vi.fn(({ payload }) =>
278
- Promise.resolve(response(payload.cursor)),
279
- );
267
+ const request = vi.fn(({ payload }) => Promise.resolve(response(payload.cursor)));
280
268
  const hook = await renderPreviewHook({
281
269
  client: asClient(request),
282
270
  target,
@@ -286,11 +274,7 @@ describe('storage entry query hooks', () => {
286
274
  await act(async () => {
287
275
  await vi.waitFor(() => expect(hook.result.isSuccess).toBe(true));
288
276
  });
289
- const otherQueryKey = storageEntryPreviewQueryKey(
290
- otherTarget,
291
- 'other',
292
- 'ascending',
293
- );
277
+ const otherQueryKey = storageEntryPreviewQueryKey(otherTarget, 'other', 'ascending');
294
278
  hook.queryClient.setQueryDefaults(otherQueryKey, { gcTime: Infinity });
295
279
  hook.queryClient.setQueryData(otherQueryKey, {
296
280
  pages: [response(undefined)],
@@ -1,10 +1,6 @@
1
- import { Button, ConfirmDialog, Dialog, Field, Input } from '@rozenite/ui';
1
+ import { Button, Dialog, Field, Input, useConfirmDialog } from '@rozenite/ui';
2
2
  import { useEffect, useState } from 'react';
3
- import type {
4
- StorageEntry,
5
- StorageEntryType,
6
- StorageEntryValue,
7
- } from '../shared/types';
3
+ import type { StorageEntry, StorageEntryType, StorageEntryValue } from '../shared/types';
8
4
  import { TypedValueEditor } from './typed-value-editor';
9
5
  import { defaultValueForType } from './type-conversion';
10
6
 
@@ -49,9 +45,7 @@ export const AddEntryDialog = ({
49
45
  const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
50
46
  defaultValueForType(initialType),
51
47
  );
52
- const [alert, setAlert] = useState<{ title: string; message: string } | null>(
53
- null,
54
- );
48
+ const confirm = useConfirmDialog();
55
49
 
56
50
  // Reset state every time the dialog opens, so a previous session's
57
51
  // type / value doesn't bleed in.
@@ -72,21 +66,23 @@ export const AddEntryDialog = ({
72
66
  onClose();
73
67
  };
74
68
 
75
- const handleAdd = () => {
69
+ const handleAdd = async () => {
76
70
  if (!newEntryKey.trim() || currentValue === null) return;
77
71
 
78
72
  if (!isCurrentTypeSupported) {
79
- setAlert({
73
+ await confirm({
74
+ variant: 'alert',
80
75
  title: 'Unsupported Type',
81
- message: 'Selected type is not supported by this storage.',
76
+ description: 'Selected type is not supported by this storage.',
82
77
  });
83
78
  return;
84
79
  }
85
80
 
86
81
  if (existingKeys.includes(newEntryKey)) {
87
- setAlert({
82
+ await confirm({
83
+ variant: 'alert',
88
84
  title: 'Key Already Exists',
89
- message: 'An entry with this key already exists.',
85
+ description: 'An entry with this key already exists.',
90
86
  });
91
87
  return;
92
88
  }
@@ -96,85 +92,68 @@ export const AddEntryDialog = ({
96
92
  };
97
93
 
98
94
  const handleKeyDown = (event: React.KeyboardEvent) => {
99
- if (
100
- event.key === 'Enter' &&
101
- newEntryKey.trim() &&
102
- currentType !== 'buffer'
103
- ) {
104
- handleAdd();
95
+ if (event.key === 'Enter' && newEntryKey.trim() && currentType !== 'buffer') {
96
+ void handleAdd();
105
97
  }
106
98
  };
107
99
 
108
100
  // Unsavable when no key, no supported type, or when the value is
109
101
  // null — the hex editor signals invalid / empty hex via null.
110
- const isAddDisabled =
111
- !newEntryKey.trim() || !isCurrentTypeSupported || currentValue === null;
102
+ const isAddDisabled = !newEntryKey.trim() || !isCurrentTypeSupported || currentValue === null;
112
103
 
113
104
  return (
114
- <>
115
- <Dialog
116
- open={isOpen}
117
- onOpenChange={(open) => {
118
- if (!open) resetAndClose();
119
- }}
120
- >
121
- <Dialog.Content onKeyDown={handleKeyDown}>
122
- <Dialog.Header>
123
- <Dialog.Title>Add New Entry</Dialog.Title>
124
- </Dialog.Header>
125
-
126
- <div className="flex flex-col gap-4">
127
- <Field>
128
- <Field.Label htmlFor="new-entry-key">Key</Field.Label>
129
- <Input
130
- id="new-entry-key"
131
- value={newEntryKey}
132
- onChange={(event) => setNewEntryKey(event.target.value)}
133
- placeholder="Enter key name"
134
- autoFocus
135
- />
136
- </Field>
137
-
138
- <Field>
139
- <Field.Label htmlFor="new-entry-value">Value</Field.Label>
140
- <TypedValueEditor
141
- supportedTypes={supportedTypes}
142
- type={currentType}
143
- value={currentValue}
144
- onChange={(nextType, nextValue) => {
145
- setCurrentType(nextType);
146
- setCurrentValue(nextValue);
147
- }}
148
- inputId="new-entry-value"
149
- />
150
- {!isCurrentTypeSupported && (
151
- <Field.Description className="text-destructive">
152
- Selected type is not supported by this storage.
153
- </Field.Description>
154
- )}
155
- </Field>
156
- </div>
157
-
158
- <Dialog.Footer>
159
- <Button variant="outline" onClick={resetAndClose}>
160
- Cancel
161
- </Button>
162
- <Button onClick={handleAdd} disabled={isAddDisabled}>
163
- Add Entry
164
- </Button>
165
- </Dialog.Footer>
166
- </Dialog.Content>
167
- </Dialog>
168
-
169
- <ConfirmDialog
170
- open={alert !== null}
171
- onOpenChange={(open) => {
172
- if (!open) setAlert(null);
173
- }}
174
- variant="alert"
175
- title={alert?.title ?? ''}
176
- description={alert?.message}
177
- />
178
- </>
105
+ <Dialog
106
+ open={isOpen}
107
+ onOpenChange={(open) => {
108
+ if (!open) resetAndClose();
109
+ }}
110
+ >
111
+ <Dialog.Content onKeyDown={handleKeyDown}>
112
+ <Dialog.Header>
113
+ <Dialog.Title>Add New Entry</Dialog.Title>
114
+ </Dialog.Header>
115
+
116
+ <div className="flex flex-col gap-4">
117
+ <Field>
118
+ <Field.Label htmlFor="new-entry-key">Key</Field.Label>
119
+ <Input
120
+ id="new-entry-key"
121
+ value={newEntryKey}
122
+ onChange={(event) => setNewEntryKey(event.target.value)}
123
+ placeholder="Enter key name"
124
+ autoFocus
125
+ />
126
+ </Field>
127
+
128
+ <Field>
129
+ <Field.Label htmlFor="new-entry-value">Value</Field.Label>
130
+ <TypedValueEditor
131
+ supportedTypes={supportedTypes}
132
+ type={currentType}
133
+ value={currentValue}
134
+ onChange={(nextType, nextValue) => {
135
+ setCurrentType(nextType);
136
+ setCurrentValue(nextValue);
137
+ }}
138
+ inputId="new-entry-value"
139
+ />
140
+ {!isCurrentTypeSupported && (
141
+ <Field.Description className="text-danger">
142
+ Selected type is not supported by this storage.
143
+ </Field.Description>
144
+ )}
145
+ </Field>
146
+ </div>
147
+
148
+ <Dialog.Footer>
149
+ <Button tone="neutral" variant="outline" onClick={resetAndClose}>
150
+ Cancel
151
+ </Button>
152
+ <Button onClick={() => void handleAdd()} disabled={isAddDisabled}>
153
+ Add Entry
154
+ </Button>
155
+ </Dialog.Footer>
156
+ </Dialog.Content>
157
+ </Dialog>
179
158
  );
180
159
  };
@@ -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
 
@@ -20,9 +15,7 @@ export type EditorAction =
20
15
  | { type: 'switch-mode'; mode: EditorMode }
21
16
  | { type: 'replace-bytes'; bytes: number[] };
22
17
 
23
- export type Validation =
24
- | { ok: true; bytes: number[] }
25
- | { ok: false; reason: string };
18
+ export type Validation = { ok: true; bytes: number[] } | { ok: false; reason: string };
26
19
 
27
20
  const encode = (bytes: readonly number[], mode: EditorMode): string =>
28
21
  mode === 'hex' ? bytesToGroupedHex(bytes) : bytesToBase64(bytes);
@@ -30,20 +23,12 @@ const encode = (bytes: readonly number[], mode: EditorMode): string =>
30
23
  const parse = (text: string, mode: EditorMode) =>
31
24
  mode === 'hex' ? hexInputToBytes(text) : base64ToBytes(text);
32
25
 
33
- const parsedToState = (
34
- text: string,
35
- mode: EditorMode,
36
- ): Pick<EditorState, 'bytes' | 'error'> => {
26
+ const parsedToState = (text: string, mode: EditorMode): Pick<EditorState, 'bytes' | 'error'> => {
37
27
  const result = parse(text, mode);
38
- return result.ok
39
- ? { bytes: result.value, error: null }
40
- : { bytes: null, error: result.error };
28
+ return result.ok ? { bytes: result.value, error: null } : { bytes: null, error: result.error };
41
29
  };
42
30
 
43
- export const initialState = (args: {
44
- initialBytes?: number[];
45
- mode?: EditorMode;
46
- }): EditorState => {
31
+ export const initialState = (args: { initialBytes?: number[]; mode?: EditorMode }): EditorState => {
47
32
  const mode = args.mode ?? 'hex';
48
33
  if (args.initialBytes && args.initialBytes.length > 0) {
49
34
  return {
@@ -61,10 +46,7 @@ export const initialState = (args: {
61
46
  };
62
47
  };
63
48
 
64
- export const reduce = (
65
- state: EditorState,
66
- action: EditorAction,
67
- ): EditorState => {
49
+ export const reduce = (state: EditorState, action: EditorAction): EditorState => {
68
50
  switch (action.type) {
69
51
  case 'set-text': {
70
52
  return {
@@ -4,11 +4,7 @@ import { EditorView, keymap } from '@codemirror/view';
4
4
  import { cn } from '@rozenite/ui';
5
5
  import { useEffect, useReducer, useRef, useState } from 'react';
6
6
  import { compactAsciiPreview } from './binary';
7
- import {
8
- initialState,
9
- reduce,
10
- type EditorMode,
11
- } from './binary-value-editor-state';
7
+ import { initialState, reduce, type EditorMode } from './binary-value-editor-state';
12
8
 
13
9
  export type BinaryValueEditorProps = {
14
10
  initialBytes?: number[];
@@ -57,13 +53,8 @@ const ModeButton = ({
57
53
  </button>
58
54
  );
59
55
 
60
- export const BinaryValueEditor = ({
61
- initialBytes,
62
- onChange,
63
- }: BinaryValueEditorProps) => {
64
- const [state, dispatch] = useReducer(reduce, undefined, () =>
65
- initialState({}),
66
- );
56
+ export const BinaryValueEditor = ({ initialBytes, onChange }: BinaryValueEditorProps) => {
57
+ const [state, dispatch] = useReducer(reduce, undefined, () => initialState({}));
67
58
  const initialBytesRef = useRef(initialBytes);
68
59
  const [isPreparing, setIsPreparing] = useState(
69
60
  Boolean(initialBytesRef.current && initialBytesRef.current.length > 0),
@@ -108,9 +99,7 @@ export const BinaryValueEditor = ({
108
99
  if (!update.docChanged) return;
109
100
  const newText = update.state.doc.toString();
110
101
  if (newText === stateRef.current.text) return;
111
- const isPaste = update.transactions.some((tr) =>
112
- tr.isUserEvent('input.paste'),
113
- );
102
+ const isPaste = update.transactions.some((tr) => tr.isUserEvent('input.paste'));
114
103
  dispatch({
115
104
  type: isPaste ? 'normalize-paste' : 'set-text',
116
105
  text: newText,
@@ -181,11 +170,10 @@ export const BinaryValueEditor = ({
181
170
  <div className="text-muted-foreground">{byteCount} bytes</div>
182
171
  {asciiPreview && (
183
172
  <div className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
184
- ASCII:{' '}
185
- <span className="font-mono text-foreground">{asciiPreview}</span>
173
+ ASCII: <span className="font-mono text-foreground">{asciiPreview}</span>
186
174
  </div>
187
175
  )}
188
- {state.error && <div className="text-destructive">{state.error}</div>}
176
+ {state.error && <div className="text-danger">{state.error}</div>}
189
177
  </div>
190
178
  </div>
191
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(' ');
@@ -33,10 +28,7 @@ export type HexdumpRow = {
33
28
 
34
29
  // Produces one visible hexdump row. Unlike bytesToHexdump this never builds a
35
30
  // representation for bytes outside the requested row.
36
- export const formatHexdumpRow = (
37
- bytes: readonly number[],
38
- rowStart: number,
39
- ): HexdumpRow => {
31
+ export const formatHexdumpRow = (bytes: readonly number[], rowStart: number): HexdumpRow => {
40
32
  let left = '';
41
33
  let right = '';
42
34
  let ascii = '';
@@ -82,10 +74,7 @@ export const bytesToHexdump = (bytes: readonly number[]): string => {
82
74
  export const bytesToAsciiPreview = (bytes: readonly number[]): string =>
83
75
  bytes.map(toAsciiChar).join('');
84
76
 
85
- export const compactAsciiPreview = (
86
- bytes: readonly number[],
87
- maxBytes = 64,
88
- ): string => {
77
+ export const compactAsciiPreview = (bytes: readonly number[], maxBytes = 64): string => {
89
78
  let preview = '';
90
79
  const limit = Math.min(bytes.length, maxBytes);
91
80
  for (let index = 0; index < limit; index++) {
@@ -133,9 +122,7 @@ const TRAILING_ASCII_COLUMN_RE = /\|[^|]*\|\s*$/;
133
122
  export const hexInputToBytes = (input: string): ParseResult<number[]> => {
134
123
  const cleaned = input
135
124
  .split(/\r?\n/)
136
- .map((line) =>
137
- line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''),
138
- )
125
+ .map((line) => line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''))
139
126
  .join('')
140
127
  .replace(/0x/gi, '')
141
128
  .replace(/\s+/g, '');
@@ -1,10 +1,6 @@
1
- import { Button, ConfirmDialog, Dialog, Field } from '@rozenite/ui';
1
+ import { Button, Dialog, Field, useConfirmDialog } from '@rozenite/ui';
2
2
  import { useEffect, useRef, useState } from 'react';
3
- import type {
4
- StorageEntry,
5
- StorageEntryType,
6
- StorageEntryValue,
7
- } from '../shared/types';
3
+ import type { StorageEntry, StorageEntryType, StorageEntryValue } from '../shared/types';
8
4
  import { TypedValueEditor } from './typed-value-editor';
9
5
  import { defaultValueForType } from './type-conversion';
10
6
 
@@ -32,9 +28,7 @@ export const EditEntryDialog = ({
32
28
  const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
33
29
  defaultValueForType('string'),
34
30
  );
35
- const [alert, setAlert] = useState<{ title: string; message: string } | null>(
36
- null,
37
- );
31
+ const confirm = useConfirmDialog();
38
32
 
39
33
  useEffect(() => {
40
34
  if (entry && isOpen) {
@@ -54,13 +48,14 @@ export const EditEntryDialog = ({
54
48
  onClose();
55
49
  };
56
50
 
57
- const handleSave = () => {
51
+ const handleSave = async () => {
58
52
  if (!entry || currentValue === null) return;
59
53
 
60
54
  if (!isCurrentTypeSupported) {
61
- setAlert({
55
+ await confirm({
56
+ variant: 'alert',
62
57
  title: 'Unsupported Type',
63
- message: 'This storage does not support the selected type.',
58
+ description: 'This storage does not support the selected type.',
64
59
  });
65
60
  return;
66
61
  }
@@ -71,7 +66,7 @@ export const EditEntryDialog = ({
71
66
 
72
67
  const handleKeyDown = (event: React.KeyboardEvent) => {
73
68
  if (event.key === 'Enter' && currentType !== 'buffer') {
74
- handleSave();
69
+ void handleSave();
75
70
  }
76
71
  };
77
72
 
@@ -85,71 +80,57 @@ export const EditEntryDialog = ({
85
80
  const isSaveDisabled = !isCurrentTypeSupported || currentValue === null;
86
81
 
87
82
  return (
88
- <>
89
- <Dialog
90
- open={isOpen}
91
- onOpenChange={(open) => {
92
- if (!open) resetAndClose();
93
- }}
94
- >
95
- <Dialog.Content onKeyDown={handleKeyDown}>
96
- <Dialog.Header>
97
- <Dialog.Title>Edit Entry</Dialog.Title>
98
- </Dialog.Header>
99
-
100
- <div className="flex flex-col gap-4">
101
- <Field>
102
- <Field.Label>Key</Field.Label>
103
- <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">
104
- {entryForDisplay.key}
105
- </div>
106
- <Field.Description>
107
- Key cannot be changed during editing
83
+ <Dialog
84
+ open={isOpen}
85
+ onOpenChange={(open) => {
86
+ if (!open) resetAndClose();
87
+ }}
88
+ >
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-danger">
119
+ This storage does not support {currentType} values.
108
120
  </Field.Description>
109
- </Field>
110
-
111
- <Field>
112
- <Field.Label htmlFor="edit-entry-value">Value</Field.Label>
113
- <TypedValueEditor
114
- key={entryForDisplay.key}
115
- supportedTypes={supportedTypes}
116
- type={currentType}
117
- value={currentValue}
118
- onChange={(nextType, nextValue) => {
119
- setCurrentType(nextType);
120
- setCurrentValue(nextValue);
121
- }}
122
- inputId="edit-entry-value"
123
- autoFocus
124
- />
125
- {!isCurrentTypeSupported && (
126
- <Field.Description className="text-destructive">
127
- This storage does not support {currentType} values.
128
- </Field.Description>
129
- )}
130
- </Field>
131
- </div>
132
-
133
- <Dialog.Footer>
134
- <Button variant="outline" onClick={resetAndClose}>
135
- Cancel
136
- </Button>
137
- <Button onClick={handleSave} disabled={isSaveDisabled}>
138
- Save Changes
139
- </Button>
140
- </Dialog.Footer>
141
- </Dialog.Content>
142
- </Dialog>
143
-
144
- <ConfirmDialog
145
- open={alert !== null}
146
- onOpenChange={(open) => {
147
- if (!open) setAlert(null);
148
- }}
149
- variant="alert"
150
- title={alert?.title ?? ''}
151
- description={alert?.message}
152
- />
153
- </>
121
+ )}
122
+ </Field>
123
+ </div>
124
+
125
+ <Dialog.Footer>
126
+ <Button tone="neutral" variant="outline" onClick={resetAndClose}>
127
+ Cancel
128
+ </Button>
129
+ <Button onClick={() => void handleSave()} disabled={isSaveDisabled}>
130
+ Save Changes
131
+ </Button>
132
+ </Dialog.Footer>
133
+ </Dialog.Content>
134
+ </Dialog>
154
135
  );
155
136
  };
@@ -10,12 +10,7 @@ const TYPE_LABELS: Record<StorageEntryType, string> = {
10
10
 
11
11
  // Visual ordering of the pills. Keep `string` first because it's the
12
12
  // most common landing type after a fresh read on an MMKV key.
13
- const TYPE_ORDER: StorageEntryType[] = [
14
- 'string',
15
- 'number',
16
- 'boolean',
17
- 'buffer',
18
- ];
13
+ const TYPE_ORDER: StorageEntryType[] = ['string', 'number', 'boolean', 'buffer'];
19
14
 
20
15
  export type EditorSwitcherProps = {
21
16
  supportedTypes: StorageEntryType[];
@@ -23,11 +18,7 @@ export type EditorSwitcherProps = {
23
18
  onChange: (type: StorageEntryType) => void;
24
19
  };
25
20
 
26
- export const EditorSwitcher = ({
27
- supportedTypes,
28
- value,
29
- onChange,
30
- }: EditorSwitcherProps) => {
21
+ export const EditorSwitcher = ({ supportedTypes, value, onChange }: EditorSwitcherProps) => {
31
22
  const available = TYPE_ORDER.filter((type) => supportedTypes.includes(type));
32
23
 
33
24
  // Adaptive hide: the switcher is meaningless when the backend only