@rozenite/storage-plugin 1.9.0 → 1.10.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.
@@ -0,0 +1,168 @@
1
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
2
+ import { EditorState as CMEditorState } from '@codemirror/state';
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';
11
+
12
+ export type BinaryValueEditorProps = {
13
+ initialBytes?: number[];
14
+ onChange: (bytes: number[] | null) => void;
15
+ };
16
+
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' },
33
+ },
34
+ { dark: true },
35
+ );
36
+
37
+ const ModeButton = ({
38
+ label,
39
+ active,
40
+ onClick,
41
+ }: {
42
+ label: string;
43
+ active: boolean;
44
+ onClick: () => void;
45
+ }) => (
46
+ <button
47
+ type="button"
48
+ onClick={onClick}
49
+ className={`rounded px-2 py-1 text-xs transition-colors ${
50
+ active
51
+ ? 'bg-blue-600 text-white'
52
+ : 'bg-gray-700 text-gray-200 hover:bg-gray-600'
53
+ }`}
54
+ >
55
+ {label}
56
+ </button>
57
+ );
58
+
59
+ export const BinaryValueEditor = ({
60
+ initialBytes,
61
+ onChange,
62
+ }: BinaryValueEditorProps) => {
63
+ const [state, dispatch] = useReducer(reduce, undefined, () =>
64
+ initialState({ initialBytes }),
65
+ );
66
+
67
+ const hostRef = useRef<HTMLDivElement | null>(null);
68
+ const viewRef = useRef<EditorView | null>(null);
69
+ const stateRef = useRef(state);
70
+ stateRef.current = state;
71
+ const onChangeRef = useRef(onChange);
72
+ onChangeRef.current = onChange;
73
+
74
+ // Mount the CodeMirror view once. The update listener compares the
75
+ // doc to the latest reducer text (via stateRef) so paste-or-type
76
+ // events round-trip through the reducer instead of looping back.
77
+ useEffect(() => {
78
+ if (!hostRef.current) {
79
+ return;
80
+ }
81
+ const view = new EditorView({
82
+ state: CMEditorState.create({
83
+ doc: stateRef.current.text,
84
+ extensions: [
85
+ history(),
86
+ keymap.of([...defaultKeymap, ...historyKeymap]),
87
+ darkTheme,
88
+ EditorView.lineWrapping,
89
+ EditorView.updateListener.of((update) => {
90
+ if (!update.docChanged) return;
91
+ const newText = update.state.doc.toString();
92
+ if (newText === stateRef.current.text) return;
93
+ const isPaste = update.transactions.some((tr) =>
94
+ tr.isUserEvent('input.paste'),
95
+ );
96
+ dispatch({
97
+ type: isPaste ? 'normalize-paste' : 'set-text',
98
+ text: newText,
99
+ });
100
+ }),
101
+ ],
102
+ }),
103
+ parent: hostRef.current,
104
+ });
105
+ viewRef.current = view;
106
+ return () => {
107
+ view.destroy();
108
+ viewRef.current = null;
109
+ };
110
+ }, []);
111
+
112
+ // Push reducer text into CodeMirror only when they diverge.
113
+ // This fires for paste normalization and mode switches.
114
+ useEffect(() => {
115
+ const view = viewRef.current;
116
+ if (!view) return;
117
+ const current = view.state.doc.toString();
118
+ if (current === state.text) return;
119
+ view.dispatch({
120
+ changes: { from: 0, to: current.length, insert: state.text },
121
+ });
122
+ }, [state.text]);
123
+
124
+ useEffect(() => {
125
+ onChangeRef.current(state.bytes);
126
+ }, [state.bytes]);
127
+
128
+ const byteCount = state.bytes?.length ?? 0;
129
+ const asciiPreview = state.bytes ? bytesToAsciiPreview(state.bytes) : '';
130
+
131
+ const handleModeChange = (mode: EditorMode) => {
132
+ if (state.mode === mode) return;
133
+ dispatch({ type: 'switch-mode', mode });
134
+ };
135
+
136
+ return (
137
+ <div className="flex flex-col gap-2">
138
+ <div className="flex items-center gap-1">
139
+ <ModeButton
140
+ label="Hex"
141
+ active={state.mode === 'hex'}
142
+ onClick={() => handleModeChange('hex')}
143
+ />
144
+ <ModeButton
145
+ label="Base64"
146
+ active={state.mode === 'base64'}
147
+ onClick={() => handleModeChange('base64')}
148
+ />
149
+ </div>
150
+
151
+ <div
152
+ ref={hostRef}
153
+ className="min-h-[120px] max-h-[300px] overflow-auto rounded border border-gray-700 bg-gray-900"
154
+ />
155
+
156
+ <div className="flex flex-col gap-1 text-xs">
157
+ <div className="text-gray-400">{byteCount} bytes</div>
158
+ {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>
162
+ </div>
163
+ )}
164
+ {state.error && <div className="text-red-400">{state.error}</div>}
165
+ </div>
166
+ </div>
167
+ );
168
+ };
@@ -0,0 +1,123 @@
1
+ export type ParseResult<T> =
2
+ | { ok: true; value: T }
3
+ | { ok: false; error: string };
4
+
5
+ const BYTES_PER_LINE = 16;
6
+ const BYTES_PER_GROUP = 8;
7
+ // 16 bytes × 2 hex chars + 14 single inter-byte spaces + 1 extra
8
+ // space between groups = 32 + 14 + 2 = 48
9
+ const HEX_SECTION_WIDTH = 48;
10
+ const OFFSET_WIDTH = 8;
11
+ const ASCII_PRINTABLE_MIN = 0x20;
12
+ const ASCII_PRINTABLE_MAX = 0x7e;
13
+
14
+ const toHexPair = (byte: number): string =>
15
+ byte.toString(16).toUpperCase().padStart(2, '0');
16
+
17
+ const toAsciiChar = (byte: number): string =>
18
+ byte >= ASCII_PRINTABLE_MIN && byte <= ASCII_PRINTABLE_MAX
19
+ ? String.fromCharCode(byte)
20
+ : '.';
21
+
22
+ const formatHexLine = (slice: readonly number[]): string => {
23
+ const left = slice.slice(0, BYTES_PER_GROUP).map(toHexPair).join(' ');
24
+ const right = slice.slice(BYTES_PER_GROUP).map(toHexPair).join(' ');
25
+ return right ? `${left} ${right}` : left;
26
+ };
27
+
28
+ export const bytesToGroupedHex = (bytes: readonly number[]): string => {
29
+ const lines: string[] = [];
30
+ for (let i = 0; i < bytes.length; i += BYTES_PER_LINE) {
31
+ lines.push(formatHexLine(bytes.slice(i, i + BYTES_PER_LINE)));
32
+ }
33
+ return lines.join('\n');
34
+ };
35
+
36
+ export const bytesToHexdump = (bytes: readonly number[]): string => {
37
+ const lines: string[] = [];
38
+ for (let i = 0; i < bytes.length; i += BYTES_PER_LINE) {
39
+ const slice = bytes.slice(i, i + BYTES_PER_LINE);
40
+ const offset = i.toString(16).padStart(OFFSET_WIDTH, '0');
41
+ const hex = formatHexLine(slice).padEnd(HEX_SECTION_WIDTH, ' ');
42
+ const ascii = slice.map(toAsciiChar).join('');
43
+ lines.push(`${offset} ${hex} |${ascii}|`);
44
+ }
45
+ return lines.join('\n');
46
+ };
47
+
48
+ export const bytesToAsciiPreview = (bytes: readonly number[]): string =>
49
+ bytes.map(toAsciiChar).join('');
50
+
51
+ export const bytesToBase64 = (bytes: readonly number[]): string => {
52
+ let binary = '';
53
+ for (const byte of bytes) {
54
+ binary += String.fromCharCode(byte);
55
+ }
56
+ return btoa(binary);
57
+ };
58
+
59
+ export const compactBufferPreview = (
60
+ bytes: readonly number[],
61
+ options: { maxBytes?: number } = {},
62
+ ): string => {
63
+ const maxBytes = options.maxBytes ?? BYTES_PER_GROUP;
64
+ const sizeLabel = `${bytes.length} B`;
65
+ if (bytes.length === 0) {
66
+ return sizeLabel;
67
+ }
68
+ const shown = bytes.slice(0, maxBytes).map(toHexPair).join(' ');
69
+ if (bytes.length <= maxBytes) {
70
+ return `${shown} ${sizeLabel}`;
71
+ }
72
+ return `${shown} … ${sizeLabel}`;
73
+ };
74
+
75
+ // Matches a 4+ hex-digit offset followed by either a colon-space or
76
+ // at least two spaces. Avoids false-positives on grouped-hex lines
77
+ // where bytes are separated by single spaces.
78
+ const HEXDUMP_OFFSET_RE = /^[0-9a-fA-F]{4,16}(?::\s+|\s{2,})/;
79
+ const TRAILING_ASCII_COLUMN_RE = /\|[^|]*\|\s*$/;
80
+
81
+ export const hexInputToBytes = (input: string): ParseResult<number[]> => {
82
+ const cleaned = input
83
+ .split(/\r?\n/)
84
+ .map((line) =>
85
+ line.replace(TRAILING_ASCII_COLUMN_RE, '').replace(HEXDUMP_OFFSET_RE, ''),
86
+ )
87
+ .join('')
88
+ .replace(/0x/gi, '')
89
+ .replace(/\s+/g, '');
90
+
91
+ if (cleaned.length === 0) {
92
+ return { ok: false, error: 'Enter at least one byte.' };
93
+ }
94
+ if (!/^[0-9a-fA-F]+$/.test(cleaned)) {
95
+ return { ok: false, error: 'Hex input contains invalid characters.' };
96
+ }
97
+ if (cleaned.length % 2 !== 0) {
98
+ return { ok: false, error: 'Hex input must contain complete bytes.' };
99
+ }
100
+ const bytes: number[] = [];
101
+ for (let i = 0; i < cleaned.length; i += 2) {
102
+ bytes.push(parseInt(cleaned.slice(i, i + 2), 16));
103
+ }
104
+ return { ok: true, value: bytes };
105
+ };
106
+
107
+ export const base64ToBytes = (input: string): ParseResult<number[]> => {
108
+ const cleaned = input.replace(/\s+/g, '');
109
+ if (cleaned.length === 0) {
110
+ return { ok: false, error: 'Enter at least one byte.' };
111
+ }
112
+ let binary: string;
113
+ try {
114
+ binary = atob(cleaned);
115
+ } catch {
116
+ return { ok: false, error: 'Base64 input is invalid.' };
117
+ }
118
+ const bytes: number[] = [];
119
+ for (let i = 0; i < binary.length; i++) {
120
+ bytes.push(binary.charCodeAt(i));
121
+ }
122
+ return { ok: true, value: bytes };
123
+ };
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { X, Edit3 } from 'lucide-react';
3
3
  import type {
4
4
  StorageEntry,
@@ -6,10 +6,16 @@ import type {
6
6
  StorageEntryValue,
7
7
  } from '../shared/types';
8
8
  import { ConfirmDialog } from './confirm-dialog';
9
+ import { TypedValueEditor } from './typed-value-editor';
10
+ import { defaultValueForType } from './type-conversion';
9
11
 
10
12
  export type EditEntryDialogProps = {
11
13
  isOpen: boolean;
12
14
  onClose: () => void;
15
+ // Called with the runtime value in its native JS shape — the caller
16
+ // infers the storage type from `typeof newValue`. Lets the dialog
17
+ // change the entry's stored type (e.g. string → buffer) without a
18
+ // dedicated prop for the new type.
13
19
  onEditEntry: (key: string, newValue: StorageEntryValue) => void;
14
20
  entry: StorageEntry | null;
15
21
  supportedTypes: StorageEntryType[];
@@ -22,147 +28,73 @@ export const EditEntryDialog = ({
22
28
  entry,
23
29
  supportedTypes,
24
30
  }: EditEntryDialogProps) => {
25
- const [editValue, setEditValue] = useState('');
31
+ const [currentType, setCurrentType] = useState<StorageEntryType>('string');
32
+ const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
33
+ defaultValueForType('string'),
34
+ );
26
35
  const [confirmDialog, setConfirmDialog] = useState<{
27
36
  isOpen: boolean;
28
37
  title: string;
29
38
  message: string;
30
- type: 'confirm' | 'alert';
31
- onConfirm?: () => void;
32
- }>({ isOpen: false, title: '', message: '', type: 'alert' });
39
+ }>({ isOpen: false, title: '', message: '' });
33
40
 
34
41
  useEffect(() => {
35
42
  if (entry && isOpen) {
36
- setEditValue(Array.isArray(entry.value) ? JSON.stringify(entry.value) : String(entry.value));
43
+ setCurrentType(entry.type);
44
+ setCurrentValue(entry.value);
37
45
  }
38
46
  }, [entry, isOpen]);
39
47
 
40
- const isTypeSupported = useMemo(
41
- () => !!entry && supportedTypes.includes(entry.type),
42
- [entry, supportedTypes]
43
- );
48
+ const isCurrentTypeSupported = supportedTypes.includes(currentType);
44
49
 
45
- const resetForm = () => {
46
- setEditValue('');
50
+ const resetAndClose = () => {
51
+ setCurrentType('string');
52
+ setCurrentValue(defaultValueForType('string'));
47
53
  onClose();
48
54
  };
49
55
 
50
- const handleEditEntry = () => {
51
- if (!entry) return;
56
+ const handleSave = () => {
57
+ if (!entry || currentValue === null) return;
52
58
 
53
- if (!isTypeSupported) {
59
+ if (!isCurrentTypeSupported) {
54
60
  setConfirmDialog({
55
61
  isOpen: true,
56
62
  title: 'Unsupported Type',
57
- message: 'This storage does not support the current entry type.',
58
- type: 'alert',
59
- });
60
- return;
61
- }
62
-
63
- let newValue: StorageEntryValue;
64
-
65
- try {
66
- switch (entry.type) {
67
- case 'string':
68
- newValue = editValue;
69
- break;
70
- case 'number':
71
- newValue = Number(editValue);
72
- if (Number.isNaN(newValue)) {
73
- throw new Error('Invalid number');
74
- }
75
- break;
76
- case 'boolean':
77
- if (editValue !== 'true' && editValue !== 'false') {
78
- throw new Error('Boolean value must be "true" or "false"');
79
- }
80
- newValue = editValue === 'true';
81
- break;
82
- case 'buffer':
83
- newValue = JSON.parse(editValue);
84
- if (!Array.isArray(newValue) || !newValue.every((v) => typeof v === 'number')) {
85
- throw new Error('Buffer must be an array of numbers');
86
- }
87
- break;
88
- default:
89
- throw new Error('Invalid type');
90
- }
91
- } catch (error) {
92
- setConfirmDialog({
93
- isOpen: true,
94
- title: 'Invalid Value',
95
- message: `Invalid value for ${entry.type}: ${
96
- error instanceof Error ? error.message : 'Unknown error'
97
- }`,
98
- type: 'alert',
63
+ message: 'This storage does not support the selected type.',
99
64
  });
100
65
  return;
101
66
  }
102
67
 
103
- onEditEntry(entry.key, newValue);
104
- resetForm();
68
+ onEditEntry(entry.key, currentValue);
69
+ resetAndClose();
105
70
  };
106
71
 
107
72
  const handleKeyDown = (event: React.KeyboardEvent) => {
108
73
  if (event.key === 'Escape') {
109
- resetForm();
110
- } else if (event.key === 'Enter' && editValue.trim()) {
111
- handleEditEntry();
112
- }
113
- };
114
-
115
- const getInputType = (type: StorageEntryType) => {
116
- if (type === 'number') {
117
- return 'number';
118
- }
119
-
120
- return 'text';
121
- };
122
-
123
- const getPlaceholder = (type: StorageEntryType) => {
124
- if (type === 'string') {
125
- return 'Enter string value';
126
- }
127
-
128
- if (type === 'number') {
129
- return 'Enter number value';
130
- }
131
-
132
- if (type === 'boolean') {
133
- return 'Enter true or false';
134
- }
135
-
136
- return 'Enter array as JSON, e.g., [1, 2, 3]';
137
- };
138
-
139
- const getTypeColorClass = (type: StorageEntryType) => {
140
- if (type === 'string') {
141
- return 'bg-green-600';
142
- }
143
-
144
- if (type === 'number') {
145
- return 'bg-blue-600';
74
+ resetAndClose();
75
+ return;
146
76
  }
147
-
148
- if (type === 'boolean') {
149
- return 'bg-yellow-600';
77
+ if (event.key === 'Enter' && currentType !== 'buffer') {
78
+ handleSave();
150
79
  }
151
-
152
- return 'bg-purple-600';
153
80
  };
154
81
 
155
82
  if (!isOpen || !entry) {
156
83
  return null;
157
84
  }
158
85
 
86
+ // Unsavable when the type isn't supported, or when the current value
87
+ // is null (the hex editor signals invalid / empty hex as null —
88
+ // empty bytes are not a meaningful entry to save).
89
+ const isSaveDisabled = !isCurrentTypeSupported || currentValue === null;
90
+
159
91
  return (
160
92
  <div
161
93
  className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
162
- onClick={resetForm}
94
+ onClick={resetAndClose}
163
95
  >
164
96
  <div
165
- className="bg-gray-800 rounded-lg p-6 w-96 max-w-full mx-4"
97
+ className="bg-gray-800 rounded-lg p-6 w-[32rem] max-w-full mx-4"
166
98
  onClick={(event) => event.stopPropagation()}
167
99
  onKeyDown={handleKeyDown}
168
100
  >
@@ -172,7 +104,7 @@ export const EditEntryDialog = ({
172
104
  <h2 className="text-lg font-semibold text-gray-100">Edit Entry</h2>
173
105
  </div>
174
106
  <button
175
- onClick={resetForm}
107
+ onClick={resetAndClose}
176
108
  className="p-1 text-gray-400 hover:text-gray-200 hover:bg-gray-700 rounded transition-colors"
177
109
  title="Close dialog"
178
110
  >
@@ -182,31 +114,15 @@ export const EditEntryDialog = ({
182
114
 
183
115
  <div className="space-y-4">
184
116
  <div>
185
- <label className="block text-sm font-medium text-gray-200 mb-1">Key</label>
117
+ <label className="block text-sm font-medium text-gray-200 mb-1">
118
+ Key
119
+ </label>
186
120
  <div className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 font-mono">
187
121
  {entry.key}
188
122
  </div>
189
- <p className="text-xs text-gray-400 mt-1">Key cannot be changed during editing</p>
190
- </div>
191
-
192
- <div>
193
- <label className="block text-sm font-medium text-gray-200 mb-1">Type</label>
194
- <div className="flex items-center">
195
- <span
196
- className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
197
- entry.type
198
- )}`}
199
- >
200
- {entry.type}
201
- </span>
202
- </div>
203
- {!isTypeSupported ? (
204
- <p className="text-xs text-amber-400 mt-1">
205
- This storage does not support {entry.type} values.
206
- </p>
207
- ) : (
208
- <p className="text-xs text-gray-400 mt-1">Type cannot be changed during editing</p>
209
- )}
123
+ <p className="text-xs text-gray-400 mt-1">
124
+ Key cannot be changed during editing
125
+ </p>
210
126
  </div>
211
127
 
212
128
  <div>
@@ -216,31 +132,20 @@ export const EditEntryDialog = ({
216
132
  >
217
133
  Value
218
134
  </label>
219
- {entry.type === 'boolean' ? (
220
- <select
221
- id="edit-entry-value"
222
- value={editValue}
223
- onChange={(event) => setEditValue(event.target.value)}
224
- className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
225
- autoFocus
226
- >
227
- <option value="true">true</option>
228
- <option value="false">false</option>
229
- </select>
230
- ) : (
231
- <input
232
- id="edit-entry-value"
233
- type={getInputType(entry.type)}
234
- value={editValue}
235
- onChange={(event) => setEditValue(event.target.value)}
236
- placeholder={getPlaceholder(entry.type)}
237
- className="w-full px-3 py-2 text-sm bg-gray-700 border border-gray-600 rounded text-gray-100 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
238
- autoFocus
239
- />
240
- )}
241
- {entry.type === 'buffer' && (
242
- <p className="text-xs text-gray-400 mt-1">
243
- Enter as JSON array of numbers, e.g., [1, 2, 3, 255]
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.
244
149
  </p>
245
150
  )}
246
151
  </div>
@@ -248,14 +153,14 @@ export const EditEntryDialog = ({
248
153
 
249
154
  <div className="flex items-center justify-end gap-2 mt-6">
250
155
  <button
251
- onClick={resetForm}
156
+ onClick={resetAndClose}
252
157
  className="px-4 py-2 text-sm text-gray-300 hover:text-white hover:bg-gray-700 rounded transition-colors"
253
158
  >
254
159
  Cancel
255
160
  </button>
256
161
  <button
257
- onClick={handleEditEntry}
258
- disabled={!editValue.trim() || !isTypeSupported}
162
+ onClick={handleSave}
163
+ disabled={isSaveDisabled}
259
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"
260
165
  >
261
166
  Save Changes
@@ -265,15 +170,13 @@ export const EditEntryDialog = ({
265
170
 
266
171
  <ConfirmDialog
267
172
  isOpen={confirmDialog.isOpen}
268
- onClose={() => setConfirmDialog((previous) => ({ ...previous, isOpen: false }))}
269
- onConfirm={() => {
270
- if (confirmDialog.onConfirm) {
271
- confirmDialog.onConfirm();
272
- }
273
- }}
173
+ onClose={() =>
174
+ setConfirmDialog((previous) => ({ ...previous, isOpen: false }))
175
+ }
176
+ onConfirm={() => {}}
274
177
  title={confirmDialog.title}
275
178
  message={confirmDialog.message}
276
- type={confirmDialog.type}
179
+ type="alert"
277
180
  />
278
181
  </div>
279
182
  );
@@ -15,6 +15,7 @@ import type {
15
15
  StorageEntryType,
16
16
  StorageEntryValue,
17
17
  } from '../shared/types';
18
+ import { compactBufferPreview } from './binary';
18
19
  import { ConfirmDialog } from './confirm-dialog';
19
20
  import { EditEntryDialog } from './edit-entry-dialog';
20
21
 
@@ -64,7 +65,7 @@ export const EditableTable = ({
64
65
  <div className="flex items-center">
65
66
  <span
66
67
  className={`px-2 py-1 text-xs font-medium rounded text-white ${getTypeColorClass(
67
- type
68
+ type,
68
69
  )}`}
69
70
  >
70
71
  {type}
@@ -110,7 +111,7 @@ export const EditableTable = ({
110
111
  ),
111
112
  }),
112
113
  ],
113
- [onDeleteEntry]
114
+ [onDeleteEntry],
114
115
  );
115
116
 
116
117
  const table = useReactTable({
@@ -186,7 +187,7 @@ export const EditableTable = ({
186
187
  ? null
187
188
  : flexRender(
188
189
  header.column.columnDef.header,
189
- header.getContext()
190
+ header.getContext(),
190
191
  )}
191
192
  {header.column.getCanSort() && (
192
193
  <span className="text-gray-500">
@@ -283,16 +284,17 @@ const formatValue = (entry: StorageEntry) => {
283
284
 
284
285
  if (entry.type === 'boolean') {
285
286
  return (
286
- <span className={`font-mono ${entry.value ? 'text-green-400' : 'text-red-400'}`}>
287
+ <span
288
+ className={`font-mono ${entry.value ? 'text-green-400' : 'text-red-400'}`}
289
+ >
287
290
  {entry.value ? 'true' : 'false'}
288
291
  </span>
289
292
  );
290
293
  }
291
294
 
292
- const displayValue =
293
- entry.value.length > 5
294
- ? `[${entry.value.slice(0, 5).join(', ')}, ...${entry.value.length - 5} more]`
295
- : `[${entry.value.join(', ')}]`;
296
-
297
- return <span className="text-purple-300 font-mono">{displayValue}</span>;
295
+ return (
296
+ <span className="text-purple-300 font-mono">
297
+ {compactBufferPreview(entry.value)}
298
+ </span>
299
+ );
298
300
  };