@rozenite/storage-plugin 1.13.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +6 -1
- package/dist/devtools/assets/panel-B6Wp0eie.css +1 -0
- package/dist/devtools/assets/panel-D7MFc4HG.js +32 -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 -64
- package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
- package/dist/react-native/chunks/secure-storage.require.js +22 -14
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -315
- package/dist/react-native/index.d.ts +154 -18
- package/dist/react-native/index.js +10 -2
- 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 +13 -15
- package/react-native.ts +29 -14
- package/rozenite.config.ts +78 -0
- package/src/react-native/__tests__/entry-preview-pagination.test.ts +262 -0
- package/src/react-native/__tests__/export-snapshot.test.ts +142 -0
- package/src/react-native/__tests__/full-entry-request.test.ts +155 -0
- package/src/react-native/__tests__/import.test.ts +172 -10
- package/src/react-native/__tests__/storage-discovery.test.ts +155 -0
- package/src/react-native/__tests__/storage-view.test.ts +172 -0
- package/src/react-native/__tests__/stress-verification.test.ts +153 -0
- package/src/react-native/__tests__/use-storage-agent-tools.test.ts +231 -0
- package/src/react-native/adapters/async-storage.ts +9 -7
- package/src/react-native/adapters/mmkv.ts +5 -0
- package/src/react-native/adapters/secure-storage.ts +9 -1
- package/src/react-native/entry-preview-pagination.ts +277 -0
- package/src/react-native/export-snapshot.ts +94 -0
- package/src/react-native/full-entry-request.ts +96 -0
- package/src/react-native/import.ts +190 -16
- package/src/react-native/storage-discovery.ts +96 -0
- package/src/react-native/storage-view.ts +45 -103
- package/src/react-native/useRozeniteStoragePlugin.ts +108 -72
- package/src/react-native/useStorageAgentTools.ts +207 -86
- package/src/shared/__tests__/entry-preview.test.ts +117 -0
- package/src/shared/__tests__/snapshot.test.ts +21 -0
- package/src/shared/agent-tools.ts +27 -11
- package/src/shared/entry-preview.ts +59 -0
- package/src/shared/messaging.ts +136 -21
- package/src/shared/snapshot.ts +8 -1
- package/src/shared/types.ts +20 -1
- package/src/ui/__tests__/binary-value-editor-state.test.ts +13 -0
- package/src/ui/__tests__/binary.test.ts +18 -0
- package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
- package/src/ui/__tests__/large-value-viewer.test.tsx +63 -0
- package/src/ui/__tests__/panel.test.tsx +409 -0
- package/src/ui/__tests__/storage-groups.test.ts +91 -0
- package/src/ui/__tests__/use-storage-entries.test.tsx +353 -0
- package/src/ui/add-entry-dialog.tsx +65 -105
- package/src/ui/binary-value-editor-state.ts +10 -1
- package/src/ui/binary-value-editor.tsx +57 -33
- package/src/ui/binary.ts +56 -4
- package/src/ui/edit-entry-dialog.tsx +71 -99
- package/src/ui/editor-switcher.tsx +7 -5
- package/src/ui/entry-detail-dialog.tsx +69 -179
- package/src/ui/format-value.tsx +32 -0
- package/src/ui/globals.css +1 -123
- package/src/ui/import-dialog.tsx +66 -91
- package/src/ui/large-value-viewer-state.ts +32 -0
- package/src/ui/large-value-viewer.tsx +71 -0
- package/src/ui/panel.tsx +724 -496
- package/src/ui/query-client.tsx +34 -0
- package/src/ui/storage-groups.ts +66 -0
- package/src/ui/typed-value-editor.tsx +14 -15
- package/src/ui/use-storage-entries.ts +207 -0
- 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,8 +1,9 @@
|
|
|
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 {
|
|
4
|
+
import { cn } from '@rozenite/ui';
|
|
5
|
+
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
6
|
+
import { compactAsciiPreview } from './binary';
|
|
6
7
|
import {
|
|
7
8
|
initialState,
|
|
8
9
|
reduce,
|
|
@@ -14,25 +15,24 @@ export type BinaryValueEditorProps = {
|
|
|
14
15
|
onChange: (bytes: number[] | null) => void;
|
|
15
16
|
};
|
|
16
17
|
|
|
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' },
|
|
18
|
+
// Pulls colors from the shared design tokens (set by `PluginShell` on its
|
|
19
|
+
// root as CSS custom properties) so the editor follows the active
|
|
20
|
+
// light/dark theme instead of being locked to one palette.
|
|
21
|
+
const editorTheme = EditorView.theme({
|
|
22
|
+
'&': {
|
|
23
|
+
color: 'var(--foreground)',
|
|
24
|
+
backgroundColor: 'transparent',
|
|
25
|
+
fontSize: '12px',
|
|
33
26
|
},
|
|
34
|
-
|
|
35
|
-
)
|
|
27
|
+
'.cm-content': {
|
|
28
|
+
caretColor: 'var(--ring)',
|
|
29
|
+
fontFamily: 'ui-monospace, "Cascadia Mono", "Fira Code", Menlo, monospace',
|
|
30
|
+
padding: '8px',
|
|
31
|
+
},
|
|
32
|
+
'.cm-focused': { outline: 'none' },
|
|
33
|
+
'.cm-gutters': { display: 'none' },
|
|
34
|
+
'.cm-scroller': { overflow: 'auto' },
|
|
35
|
+
});
|
|
36
36
|
|
|
37
37
|
const ModeButton = ({
|
|
38
38
|
label,
|
|
@@ -46,11 +46,12 @@ const ModeButton = ({
|
|
|
46
46
|
<button
|
|
47
47
|
type="button"
|
|
48
48
|
onClick={onClick}
|
|
49
|
-
className={
|
|
49
|
+
className={cn(
|
|
50
|
+
'rounded-md px-2 py-1 text-xs font-medium transition-colors',
|
|
50
51
|
active
|
|
51
|
-
? 'bg-
|
|
52
|
-
: '
|
|
53
|
-
}
|
|
52
|
+
? 'bg-primary text-primary-foreground'
|
|
53
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
54
|
+
)}
|
|
54
55
|
>
|
|
55
56
|
{label}
|
|
56
57
|
</button>
|
|
@@ -61,7 +62,11 @@ export const BinaryValueEditor = ({
|
|
|
61
62
|
onChange,
|
|
62
63
|
}: BinaryValueEditorProps) => {
|
|
63
64
|
const [state, dispatch] = useReducer(reduce, undefined, () =>
|
|
64
|
-
initialState({
|
|
65
|
+
initialState({}),
|
|
66
|
+
);
|
|
67
|
+
const initialBytesRef = useRef(initialBytes);
|
|
68
|
+
const [isPreparing, setIsPreparing] = useState(
|
|
69
|
+
Boolean(initialBytesRef.current && initialBytesRef.current.length > 0),
|
|
65
70
|
);
|
|
66
71
|
|
|
67
72
|
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -71,6 +76,19 @@ export const BinaryValueEditor = ({
|
|
|
71
76
|
const onChangeRef = useRef(onChange);
|
|
72
77
|
onChangeRef.current = onChange;
|
|
73
78
|
|
|
79
|
+
// Formatting a complete binary editor document is unavoidable when editing,
|
|
80
|
+
// but not when the dialog first opens. Yield once so the dialog can paint a
|
|
81
|
+
// size-aware loading state before constructing a potentially huge document.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const initialBytes = initialBytesRef.current;
|
|
84
|
+
if (!initialBytes?.length) return;
|
|
85
|
+
const timeout = window.setTimeout(() => {
|
|
86
|
+
dispatch({ type: 'replace-bytes', bytes: initialBytes });
|
|
87
|
+
setIsPreparing(false);
|
|
88
|
+
}, 0);
|
|
89
|
+
return () => window.clearTimeout(timeout);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
74
92
|
// Mount the CodeMirror view once. The update listener compares the
|
|
75
93
|
// doc to the latest reducer text (via stateRef) so paste-or-type
|
|
76
94
|
// events round-trip through the reducer instead of looping back.
|
|
@@ -84,7 +102,7 @@ export const BinaryValueEditor = ({
|
|
|
84
102
|
extensions: [
|
|
85
103
|
history(),
|
|
86
104
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
87
|
-
|
|
105
|
+
editorTheme,
|
|
88
106
|
EditorView.lineWrapping,
|
|
89
107
|
EditorView.updateListener.of((update) => {
|
|
90
108
|
if (!update.docChanged) return;
|
|
@@ -125,8 +143,8 @@ export const BinaryValueEditor = ({
|
|
|
125
143
|
onChangeRef.current(state.bytes);
|
|
126
144
|
}, [state.bytes]);
|
|
127
145
|
|
|
128
|
-
const byteCount = state.bytes?.length ?? 0;
|
|
129
|
-
const asciiPreview = state.bytes ?
|
|
146
|
+
const byteCount = state.bytes?.length ?? initialBytes?.length ?? 0;
|
|
147
|
+
const asciiPreview = state.bytes ? compactAsciiPreview(state.bytes) : '';
|
|
130
148
|
|
|
131
149
|
const handleModeChange = (mode: EditorMode) => {
|
|
132
150
|
if (state.mode === mode) return;
|
|
@@ -150,18 +168,24 @@ export const BinaryValueEditor = ({
|
|
|
150
168
|
|
|
151
169
|
<div
|
|
152
170
|
ref={hostRef}
|
|
153
|
-
className="min-h-[120px] max-h-[300px] overflow-auto rounded border border-
|
|
171
|
+
className="min-h-[120px] max-h-[300px] overflow-auto rounded-md border border-input bg-muted"
|
|
154
172
|
/>
|
|
155
173
|
|
|
174
|
+
{isPreparing ? (
|
|
175
|
+
<div className="text-xs text-muted-foreground">
|
|
176
|
+
Preparing editor for {initialBytes?.length ?? 0} bytes…
|
|
177
|
+
</div>
|
|
178
|
+
) : null}
|
|
179
|
+
|
|
156
180
|
<div className="flex flex-col gap-1 text-xs">
|
|
157
|
-
<div className="text-
|
|
181
|
+
<div className="text-muted-foreground">{byteCount} bytes</div>
|
|
158
182
|
{asciiPreview && (
|
|
159
|
-
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-
|
|
183
|
+
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
|
|
160
184
|
ASCII:{' '}
|
|
161
|
-
<span className="font-mono text-
|
|
185
|
+
<span className="font-mono text-foreground">{asciiPreview}</span>
|
|
162
186
|
</div>
|
|
163
187
|
)}
|
|
164
|
-
{state.error && <div className="text-
|
|
188
|
+
{state.error && <div className="text-destructive">{state.error}</div>}
|
|
165
189
|
</div>
|
|
166
190
|
</div>
|
|
167
191
|
);
|
package/src/ui/binary.ts
CHANGED
|
@@ -25,6 +25,40 @@ const formatHexLine = (slice: readonly number[]): string => {
|
|
|
25
25
|
return right ? `${left} ${right}` : left;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
export type HexdumpRow = {
|
|
29
|
+
offset: string;
|
|
30
|
+
hex: string;
|
|
31
|
+
ascii: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Produces one visible hexdump row. Unlike bytesToHexdump this never builds a
|
|
35
|
+
// representation for bytes outside the requested row.
|
|
36
|
+
export const formatHexdumpRow = (
|
|
37
|
+
bytes: readonly number[],
|
|
38
|
+
rowStart: number,
|
|
39
|
+
): HexdumpRow => {
|
|
40
|
+
let left = '';
|
|
41
|
+
let right = '';
|
|
42
|
+
let ascii = '';
|
|
43
|
+
const rowEnd = Math.min(rowStart + BYTES_PER_LINE, bytes.length);
|
|
44
|
+
|
|
45
|
+
for (let index = rowStart; index < rowEnd; index++) {
|
|
46
|
+
const hex = toHexPair(bytes[index]);
|
|
47
|
+
if (index < rowStart + BYTES_PER_GROUP) {
|
|
48
|
+
left += left ? ` ${hex}` : hex;
|
|
49
|
+
} else {
|
|
50
|
+
right += right ? ` ${hex}` : hex;
|
|
51
|
+
}
|
|
52
|
+
ascii += toAsciiChar(bytes[index]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
offset: rowStart.toString(16).padStart(OFFSET_WIDTH, '0'),
|
|
57
|
+
hex: right ? `${left} ${right}` : left,
|
|
58
|
+
ascii,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
28
62
|
export const bytesToGroupedHex = (bytes: readonly number[]): string => {
|
|
29
63
|
const lines: string[] = [];
|
|
30
64
|
for (let i = 0; i < bytes.length; i += BYTES_PER_LINE) {
|
|
@@ -48,12 +82,30 @@ export const bytesToHexdump = (bytes: readonly number[]): string => {
|
|
|
48
82
|
export const bytesToAsciiPreview = (bytes: readonly number[]): string =>
|
|
49
83
|
bytes.map(toAsciiChar).join('');
|
|
50
84
|
|
|
85
|
+
export const compactAsciiPreview = (
|
|
86
|
+
bytes: readonly number[],
|
|
87
|
+
maxBytes = 64,
|
|
88
|
+
): string => {
|
|
89
|
+
let preview = '';
|
|
90
|
+
const limit = Math.min(bytes.length, maxBytes);
|
|
91
|
+
for (let index = 0; index < limit; index++) {
|
|
92
|
+
preview += toAsciiChar(bytes[index]);
|
|
93
|
+
}
|
|
94
|
+
return bytes.length > maxBytes ? `${preview}…` : preview;
|
|
95
|
+
};
|
|
96
|
+
|
|
51
97
|
export const bytesToBase64 = (bytes: readonly number[]): string => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
const chunks: string[] = [];
|
|
99
|
+
const chunkSize = 8_192;
|
|
100
|
+
for (let start = 0; start < bytes.length; start += chunkSize) {
|
|
101
|
+
let chunk = '';
|
|
102
|
+
const end = Math.min(start + chunkSize, bytes.length);
|
|
103
|
+
for (let index = start; index < end; index++) {
|
|
104
|
+
chunk += String.fromCharCode(bytes[index]);
|
|
105
|
+
}
|
|
106
|
+
chunks.push(chunk);
|
|
55
107
|
}
|
|
56
|
-
return btoa(
|
|
108
|
+
return btoa(chunks.join(''));
|
|
57
109
|
};
|
|
58
110
|
|
|
59
111
|
export const compactBufferPreview = (
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Button, ConfirmDialog, Dialog, Field } from '@rozenite/ui';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import type {
|
|
4
4
|
StorageEntry,
|
|
5
5
|
StorageEntryType,
|
|
6
6
|
StorageEntryValue,
|
|
7
7
|
} from '../shared/types';
|
|
8
|
-
import { ConfirmDialog } from './confirm-dialog';
|
|
9
8
|
import { TypedValueEditor } from './typed-value-editor';
|
|
10
9
|
import { defaultValueForType } from './type-conversion';
|
|
11
10
|
|
|
@@ -28,23 +27,25 @@ export const EditEntryDialog = ({
|
|
|
28
27
|
entry,
|
|
29
28
|
supportedTypes,
|
|
30
29
|
}: EditEntryDialogProps) => {
|
|
30
|
+
const lastEntryRef = useRef<StorageEntry | null>(entry);
|
|
31
31
|
const [currentType, setCurrentType] = useState<StorageEntryType>('string');
|
|
32
32
|
const [currentValue, setCurrentValue] = useState<StorageEntryValue | null>(
|
|
33
33
|
defaultValueForType('string'),
|
|
34
34
|
);
|
|
35
|
-
const [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
message: string;
|
|
39
|
-
}>({ isOpen: false, title: '', message: '' });
|
|
35
|
+
const [alert, setAlert] = useState<{ title: string; message: string } | null>(
|
|
36
|
+
null,
|
|
37
|
+
);
|
|
40
38
|
|
|
41
39
|
useEffect(() => {
|
|
42
40
|
if (entry && isOpen) {
|
|
41
|
+
lastEntryRef.current = entry;
|
|
43
42
|
setCurrentType(entry.type);
|
|
44
43
|
setCurrentValue(entry.value);
|
|
45
44
|
}
|
|
46
45
|
}, [entry, isOpen]);
|
|
47
46
|
|
|
47
|
+
const entryForDisplay = entry ?? lastEntryRef.current;
|
|
48
|
+
|
|
48
49
|
const isCurrentTypeSupported = supportedTypes.includes(currentType);
|
|
49
50
|
|
|
50
51
|
const resetAndClose = () => {
|
|
@@ -57,8 +58,7 @@ export const EditEntryDialog = ({
|
|
|
57
58
|
if (!entry || currentValue === null) return;
|
|
58
59
|
|
|
59
60
|
if (!isCurrentTypeSupported) {
|
|
60
|
-
|
|
61
|
-
isOpen: true,
|
|
61
|
+
setAlert({
|
|
62
62
|
title: 'Unsupported Type',
|
|
63
63
|
message: 'This storage does not support the selected type.',
|
|
64
64
|
});
|
|
@@ -70,16 +70,12 @@ export const EditEntryDialog = ({
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
73
|
-
if (event.key === 'Escape') {
|
|
74
|
-
resetAndClose();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
73
|
if (event.key === 'Enter' && currentType !== 'buffer') {
|
|
78
74
|
handleSave();
|
|
79
75
|
}
|
|
80
76
|
};
|
|
81
77
|
|
|
82
|
-
if (!
|
|
78
|
+
if (!entryForDisplay) {
|
|
83
79
|
return null;
|
|
84
80
|
}
|
|
85
81
|
|
|
@@ -89,95 +85,71 @@ export const EditEntryDialog = ({
|
|
|
89
85
|
const isSaveDisabled = !isCurrentTypeSupported || currentValue === null;
|
|
90
86
|
|
|
91
87
|
return (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
onClick={(event) => event.stopPropagation()}
|
|
99
|
-
onKeyDown={handleKeyDown}
|
|
88
|
+
<>
|
|
89
|
+
<Dialog
|
|
90
|
+
open={isOpen}
|
|
91
|
+
onOpenChange={(open) => {
|
|
92
|
+
if (!open) resetAndClose();
|
|
93
|
+
}}
|
|
100
94
|
>
|
|
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
|
-
|
|
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
|
|
108
|
+
</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>
|
|
126
131
|
</div>
|
|
127
132
|
|
|
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>
|
|
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>
|
|
170
143
|
|
|
171
144
|
<ConfirmDialog
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
title={
|
|
178
|
-
|
|
179
|
-
type="alert"
|
|
145
|
+
open={alert !== null}
|
|
146
|
+
onOpenChange={(open) => {
|
|
147
|
+
if (!open) setAlert(null);
|
|
148
|
+
}}
|
|
149
|
+
variant="alert"
|
|
150
|
+
title={alert?.title ?? ''}
|
|
151
|
+
description={alert?.message}
|
|
180
152
|
/>
|
|
181
|
-
|
|
153
|
+
</>
|
|
182
154
|
);
|
|
183
155
|
};
|
|
@@ -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> = {
|
|
@@ -39,7 +40,7 @@ export const EditorSwitcher = ({
|
|
|
39
40
|
<div
|
|
40
41
|
role="tablist"
|
|
41
42
|
aria-label="Edit as"
|
|
42
|
-
className="inline-flex rounded border border-
|
|
43
|
+
className="inline-flex overflow-hidden rounded-md border border-border"
|
|
43
44
|
>
|
|
44
45
|
{available.map((type) => (
|
|
45
46
|
<button
|
|
@@ -48,11 +49,12 @@ export const EditorSwitcher = ({
|
|
|
48
49
|
role="tab"
|
|
49
50
|
aria-selected={value === type}
|
|
50
51
|
onClick={() => onChange(type)}
|
|
51
|
-
className={
|
|
52
|
+
className={cn(
|
|
53
|
+
'px-3 py-1 text-xs font-medium transition-colors',
|
|
52
54
|
value === type
|
|
53
|
-
? 'bg-
|
|
54
|
-
: 'text-
|
|
55
|
-
}
|
|
55
|
+
? 'bg-primary text-primary-foreground'
|
|
56
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
|
57
|
+
)}
|
|
56
58
|
>
|
|
57
59
|
{TYPE_LABELS[type]}
|
|
58
60
|
</button>
|