@rozenite/storage-plugin 1.12.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -1
  3. package/dist/devtools/assets/panel-B6Wp0eie.css +1 -0
  4. package/dist/devtools/assets/panel-D7MFc4HG.js +32 -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 -64
  10. package/dist/react-native/chunks/secure-storage.require.cjs +1 -1
  11. package/dist/react-native/chunks/secure-storage.require.js +22 -14
  12. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  13. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +863 -315
  14. package/dist/react-native/index.d.ts +154 -18
  15. package/dist/react-native/index.js +10 -2
  16. package/dist/rozenite.json +1 -1
  17. package/dist/sdk/index.cjs +1 -1
  18. package/dist/sdk/index.d.ts +13 -5
  19. package/dist/sdk/index.js +21 -12
  20. package/package.json +13 -15
  21. package/react-native.ts +29 -14
  22. package/rozenite.config.ts +78 -0
  23. package/src/react-native/__tests__/entry-preview-pagination.test.ts +262 -0
  24. package/src/react-native/__tests__/export-snapshot.test.ts +142 -0
  25. package/src/react-native/__tests__/full-entry-request.test.ts +155 -0
  26. package/src/react-native/__tests__/import.test.ts +172 -10
  27. package/src/react-native/__tests__/storage-discovery.test.ts +155 -0
  28. package/src/react-native/__tests__/storage-view.test.ts +172 -0
  29. package/src/react-native/__tests__/stress-verification.test.ts +153 -0
  30. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +231 -0
  31. package/src/react-native/adapters/async-storage.ts +9 -7
  32. package/src/react-native/adapters/mmkv.ts +5 -0
  33. package/src/react-native/adapters/secure-storage.ts +9 -1
  34. package/src/react-native/entry-preview-pagination.ts +277 -0
  35. package/src/react-native/export-snapshot.ts +94 -0
  36. package/src/react-native/full-entry-request.ts +96 -0
  37. package/src/react-native/import.ts +190 -16
  38. package/src/react-native/storage-discovery.ts +96 -0
  39. package/src/react-native/storage-view.ts +45 -103
  40. package/src/react-native/useRozeniteStoragePlugin.ts +108 -72
  41. package/src/react-native/useStorageAgentTools.ts +207 -86
  42. package/src/shared/__tests__/entry-preview.test.ts +117 -0
  43. package/src/shared/__tests__/snapshot.test.ts +21 -0
  44. package/src/shared/agent-tools.ts +27 -11
  45. package/src/shared/entry-preview.ts +59 -0
  46. package/src/shared/messaging.ts +136 -21
  47. package/src/shared/snapshot.ts +8 -1
  48. package/src/shared/types.ts +20 -1
  49. package/src/ui/__tests__/binary-value-editor-state.test.ts +13 -0
  50. package/src/ui/__tests__/binary.test.ts +18 -0
  51. package/src/ui/__tests__/large-value-viewer-state.test.ts +21 -0
  52. package/src/ui/__tests__/large-value-viewer.test.tsx +63 -0
  53. package/src/ui/__tests__/panel.test.tsx +409 -0
  54. package/src/ui/__tests__/storage-groups.test.ts +91 -0
  55. package/src/ui/__tests__/use-storage-entries.test.tsx +353 -0
  56. package/src/ui/add-entry-dialog.tsx +65 -105
  57. package/src/ui/binary-value-editor-state.ts +10 -1
  58. package/src/ui/binary-value-editor.tsx +57 -33
  59. package/src/ui/binary.ts +56 -4
  60. package/src/ui/edit-entry-dialog.tsx +71 -99
  61. package/src/ui/editor-switcher.tsx +7 -5
  62. package/src/ui/entry-detail-dialog.tsx +69 -179
  63. package/src/ui/format-value.tsx +32 -0
  64. package/src/ui/globals.css +1 -123
  65. package/src/ui/import-dialog.tsx +66 -91
  66. package/src/ui/large-value-viewer-state.ts +32 -0
  67. package/src/ui/large-value-viewer.tsx +71 -0
  68. package/src/ui/panel.tsx +724 -496
  69. package/src/ui/query-client.tsx +34 -0
  70. package/src/ui/storage-groups.ts +66 -0
  71. package/src/ui/typed-value-editor.tsx +14 -15
  72. package/src/ui/use-storage-entries.ts +207 -0
  73. package/tsconfig.json +5 -1
  74. package/dist/devtools/assets/panel-Bm-SWF7d.js +0 -33
  75. package/dist/devtools/assets/panel-DIqI4WSp.css +0 -1
  76. package/postcss.config.js +0 -6
  77. package/src/ui/confirm-dialog.tsx +0 -100
  78. package/src/ui/editable-table.tsx +0 -300
  79. 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 { useEffect, useReducer, useRef } from 'react';
5
- import { bytesToAsciiPreview } from './binary';
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
- 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' },
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
- { dark: true },
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={`rounded px-2 py-1 text-xs transition-colors ${
49
+ className={cn(
50
+ 'rounded-md px-2 py-1 text-xs font-medium transition-colors',
50
51
  active
51
- ? 'bg-blue-600 text-white'
52
- : 'bg-gray-700 text-gray-200 hover:bg-gray-600'
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({ initialBytes }),
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
- darkTheme,
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 ? bytesToAsciiPreview(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-gray-700 bg-gray-900"
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-gray-400">{byteCount} bytes</div>
181
+ <div className="text-muted-foreground">{byteCount} bytes</div>
158
182
  {asciiPreview && (
159
- <div className="overflow-hidden text-ellipsis whitespace-nowrap text-gray-400">
183
+ <div className="overflow-hidden text-ellipsis whitespace-nowrap text-muted-foreground">
160
184
  ASCII:{' '}
161
- <span className="font-mono text-gray-300">{asciiPreview}</span>
185
+ <span className="font-mono text-foreground">{asciiPreview}</span>
162
186
  </div>
163
187
  )}
164
- {state.error && <div className="text-red-400">{state.error}</div>}
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
- let binary = '';
53
- for (const byte of bytes) {
54
- binary += String.fromCharCode(byte);
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(binary);
108
+ return btoa(chunks.join(''));
57
109
  };
58
110
 
59
111
  export const compactBufferPreview = (
@@ -1,11 +1,10 @@
1
- import { useEffect, useState } from 'react';
2
- import { X, Edit3 } from 'lucide-react';
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 [confirmDialog, setConfirmDialog] = useState<{
36
- isOpen: boolean;
37
- title: string;
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
- setConfirmDialog({
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 (!isOpen || !entry) {
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
- <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}
88
+ <>
89
+ <Dialog
90
+ open={isOpen}
91
+ onOpenChange={(open) => {
92
+ if (!open) resetAndClose();
93
+ }}
100
94
  >
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>
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
- <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>
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
- 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"
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
- </div>
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-gray-600 overflow-hidden"
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={`px-3 py-1 text-xs transition-colors ${
52
+ className={cn(
53
+ 'px-3 py-1 text-xs font-medium transition-colors',
52
54
  value === type
53
- ? 'bg-blue-600 text-white'
54
- : 'text-gray-300 hover:bg-gray-700'
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>