@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,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 { useEffect, useReducer, useRef } from 'react';
5
- import { bytesToAsciiPreview } from './binary';
6
- import {
7
- initialState,
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
- const darkTheme = EditorView.theme(
18
- {
19
- '&': {
20
- color: '#e5e7eb',
21
- backgroundColor: '#111827',
22
- fontSize: '12px',
23
- },
24
- '.cm-content': {
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
- { dark: true },
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={`rounded px-2 py-1 text-xs transition-colors ${
45
+ className={cn(
46
+ 'rounded-md px-2 py-1 text-xs font-medium transition-colors',
50
47
  active
51
- ? 'bg-blue-600 text-white'
52
- : 'bg-gray-700 text-gray-200 hover:bg-gray-600'
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
- initialBytes,
61
- onChange,
62
- }: BinaryValueEditorProps) => {
63
- const [state, dispatch] = useReducer(reduce, undefined, () =>
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
- darkTheme,
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 ? bytesToAsciiPreview(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-gray-700 bg-gray-900"
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-gray-400">{byteCount} bytes</div>
170
+ <div className="text-muted-foreground">{byteCount} bytes</div>
158
171
  {asciiPreview && (
159
- <div className="overflow-hidden text-ellipsis whitespace-nowrap text-gray-400">
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-red-400">{state.error}</div>}
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
- let binary = '';
53
- for (const byte of bytes) {
54
- binary += String.fromCharCode(byte);
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(binary);
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 { useEffect, useState } from 'react';
2
- import { X, Edit3 } from 'lucide-react';
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 [confirmDialog, setConfirmDialog] = useState<{
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
- setConfirmDialog({
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 (!isOpen || !entry) {
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
- <div
93
- className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
94
- onClick={resetAndClose}
95
- >
96
- <div
97
- className="bg-gray-800 rounded-lg p-6 w-[32rem] max-w-full mx-4"
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
- <div className="flex items-center justify-between mb-4">
102
- <div className="flex items-center gap-2">
103
- <Edit3 className="h-5 w-5 text-blue-400" />
104
- <h2 className="text-lg font-semibold text-gray-100">Edit Entry</h2>
105
- </div>
106
- <button
107
- onClick={resetAndClose}
108
- className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
109
- title="Close dialog"
110
- >
111
- <X className="h-4 w-4" />
112
- </button>
113
- </div>
114
-
115
- <div className="space-y-4">
116
- <div>
117
- <label className="block text-sm font-medium text-gray-200 mb-1">
118
- Key
119
- </label>
120
- <div className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 font-mono">
121
- {entry.key}
122
- </div>
123
- <p className="text-xs text-gray-400 mt-1">
124
- Key cannot be changed during editing
125
- </p>
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
- <div>
129
- <label
130
- htmlFor="edit-entry-value"
131
- className="block text-sm font-medium text-gray-200 mb-1"
132
- >
133
- Value
134
- </label>
135
- <TypedValueEditor
136
- supportedTypes={supportedTypes}
137
- type={currentType}
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
- isOpen={confirmDialog.isOpen}
173
- onClose={() =>
174
- setConfirmDialog((previous) => ({ ...previous, isOpen: false }))
175
- }
176
- onConfirm={() => {}}
177
- title={confirmDialog.title}
178
- message={confirmDialog.message}
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
- </div>
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-gray-600 overflow-hidden"
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={`px-3 py-1 text-xs transition-colors ${
43
+ className={cn(
44
+ 'px-3 py-1 text-xs font-medium transition-colors',
52
45
  value === type
53
- ? 'bg-blue-600 text-white'
54
- : 'text-gray-300 hover:bg-gray-700'
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>