@rozenite/storage-plugin 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/devtools/assets/panel-D5z1BY8U.js +32 -0
  3. package/dist/devtools/assets/panel-DVlWRg6d.css +1 -0
  4. package/dist/devtools/panel.html +2 -2
  5. package/dist/react-native/chunks/index.require.js +1 -3
  6. package/dist/react-native/chunks/secure-storage.require.js +2 -6
  7. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.cjs +1 -1
  8. package/dist/react-native/chunks/useRozeniteStoragePlugin.require.js +266 -295
  9. package/dist/react-native/index.d.ts +14 -2
  10. package/dist/react-native/index.js +2 -10
  11. package/dist/rozenite.json +1 -1
  12. package/package.json +29 -29
  13. package/react-native.ts +11 -36
  14. package/rozenite.config.ts +2 -6
  15. package/sdk.ts +1 -5
  16. package/src/react-native/__tests__/entry-preview-pagination.test.ts +12 -40
  17. package/src/react-native/__tests__/export-snapshot.test.ts +2 -6
  18. package/src/react-native/__tests__/full-entry-request.test.ts +5 -17
  19. package/src/react-native/__tests__/import.test.ts +9 -8
  20. package/src/react-native/__tests__/storage-discovery.test.ts +7 -18
  21. package/src/react-native/__tests__/stress-verification.test.ts +5 -18
  22. package/src/react-native/__tests__/use-storage-agent-tools.test.ts +25 -35
  23. package/src/react-native/adapters/__tests__/mmkv.test.ts +20 -30
  24. package/src/react-native/adapters/async-storage.ts +2 -5
  25. package/src/react-native/adapters/index.ts +2 -8
  26. package/src/react-native/adapters/mmkv.ts +7 -24
  27. package/src/react-native/adapters/secure-storage.ts +2 -6
  28. package/src/react-native/entry-preview-pagination.ts +11 -27
  29. package/src/react-native/export-snapshot.ts +3 -9
  30. package/src/react-native/full-entry-request.ts +3 -8
  31. package/src/react-native/import.ts +10 -21
  32. package/src/react-native/storage-discovery.ts +2 -6
  33. package/src/react-native/storage-view.ts +2 -6
  34. package/src/react-native/useRozeniteStoragePlugin.ts +107 -119
  35. package/src/react-native/useStorageAgentTools.ts +8 -24
  36. package/src/shared/__tests__/entry-preview.test.ts +9 -27
  37. package/src/shared/__tests__/snapshot.test.ts +10 -32
  38. package/src/shared/agent-tools.ts +9 -30
  39. package/src/shared/entry-preview.ts +3 -8
  40. package/src/shared/messaging.ts +15 -11
  41. package/src/shared/snapshot.ts +4 -17
  42. package/src/shared/types.ts +2 -4
  43. package/src/ui/__tests__/binary-value-editor-state.test.ts +6 -16
  44. package/src/ui/__tests__/binary.test.ts +13 -33
  45. package/src/ui/__tests__/large-value-viewer.test.tsx +4 -13
  46. package/src/ui/__tests__/panel.test.tsx +50 -63
  47. package/src/ui/__tests__/storage-groups.test.ts +1 -4
  48. package/src/ui/__tests__/type-conversion.test.ts +8 -24
  49. package/src/ui/__tests__/use-storage-entries.test.tsx +18 -34
  50. package/src/ui/add-entry-dialog.tsx +66 -87
  51. package/src/ui/binary-value-editor-state.ts +6 -24
  52. package/src/ui/binary-value-editor.tsx +6 -18
  53. package/src/ui/binary.ts +6 -19
  54. package/src/ui/edit-entry-dialog.tsx +59 -78
  55. package/src/ui/editor-switcher.tsx +2 -11
  56. package/src/ui/entry-detail-dialog.tsx +9 -16
  57. package/src/ui/format-value.tsx +2 -9
  58. package/src/ui/import-dialog.tsx +12 -24
  59. package/src/ui/large-value-viewer-state.ts +2 -9
  60. package/src/ui/large-value-viewer.tsx +3 -9
  61. package/src/ui/panel.tsx +129 -235
  62. package/src/ui/query-client.tsx +4 -15
  63. package/src/ui/type-conversion.ts +2 -5
  64. package/src/ui/typed-value-editor.tsx +1 -5
  65. package/src/ui/use-storage-entries.ts +6 -21
  66. package/src/ui/utils.ts +1 -4
  67. package/tsconfig.json +4 -7
  68. package/dist/devtools/assets/panel-B6Wp0eie.css +0 -1
  69. package/dist/devtools/assets/panel-D7MFc4HG.js +0 -32
package/src/ui/panel.tsx CHANGED
@@ -1,32 +1,20 @@
1
1
  import { useQueryClient } from '@tanstack/react-query';
2
- import type {
3
- ColumnDef,
4
- OnChangeFn,
5
- SortingState,
6
- } from '@tanstack/react-table';
2
+ import type { ColumnDef, OnChangeFn, SortingState } from '@tanstack/react-table';
7
3
  import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
8
4
  import {
9
5
  Badge,
10
- Button,
11
- ConfirmDialog,
12
6
  EmptyState,
7
+ IconButton,
13
8
  PluginShell,
14
9
  SearchField,
15
10
  Sidebar,
16
11
  Split,
17
12
  Toolbar,
13
+ useConfirmDialog,
18
14
  VirtualizedDataTable,
19
15
  } from '@rozenite/ui';
20
- import { useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react';
21
- import {
22
- Database,
23
- Download,
24
- Edit3,
25
- Plus,
26
- RefreshCw,
27
- Trash2,
28
- Upload,
29
- } from 'lucide-react';
16
+ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react';
17
+ import { Database, Download, Edit3, Plus, RefreshCw, Trash2, Upload } from 'lucide-react';
30
18
  import type {
31
19
  StorageDiscoverStoragesResponseEvent,
32
20
  StorageEventMap,
@@ -60,15 +48,12 @@ import {
60
48
  import { buildExportFilename, downloadJson } from './utils';
61
49
  import './globals.css';
62
50
 
63
- type AlertState = { title: string; message: string };
64
51
  type FullEntryInteraction = { key: string; mode: 'detail' | 'edit' };
65
52
 
66
53
  const sameTarget = (a: StorageTarget, b: StorageTarget) =>
67
54
  a.adapterId === b.adapterId && a.storageId === b.storageId;
68
55
 
69
- const getEntryTypeFromValue = (
70
- value: StorageEntryValue,
71
- ): StorageEntry['type'] => {
56
+ const getEntryTypeFromValue = (value: StorageEntryValue): StorageEntry['type'] => {
72
57
  if (typeof value === 'string') return 'string';
73
58
  if (typeof value === 'number') return 'number';
74
59
  if (typeof value === 'boolean') return 'boolean';
@@ -88,29 +73,17 @@ const useDebouncedValue = (value: string, delay = 250) => {
88
73
 
89
74
  function StoragePanelContent() {
90
75
  const [descriptors, setDescriptors] = useState<StorageDescriptor[]>([]);
91
- const [selectedTarget, setSelectedTarget] = useState<StorageTarget | null>(
92
- null,
93
- );
76
+ const [selectedTarget, setSelectedTarget] = useState<StorageTarget | null>(null);
94
77
  const [searchTerm, setSearchTerm] = useState('');
95
- const [keySortDirection, setKeySortDirection] = useState<
96
- 'ascending' | 'descending'
97
- >('ascending');
98
- const [interaction, setInteraction] = useState<FullEntryInteraction | null>(
99
- null,
100
- );
78
+ const [keySortDirection, setKeySortDirection] = useState<'ascending' | 'descending'>('ascending');
79
+ const [interaction, setInteraction] = useState<FullEntryInteraction | null>(null);
101
80
  const [showAddDialog, setShowAddDialog] = useState(false);
102
- const [deleteKey, setDeleteKey] = useState<string | null>(null);
103
- const [showPurgeDialog, setShowPurgeDialog] = useState(false);
104
81
  const [virtualListVersion, setVirtualListVersion] = useState(0);
105
82
  const [exportState, setExportState] = useState<
106
- | { status: 'idle' }
107
- | { status: 'loading' }
108
- | { status: 'error'; message: string }
83
+ { status: 'idle' } | { status: 'loading' } | { status: 'error'; message: string }
109
84
  >({ status: 'idle' });
110
- const [importFlight, setImportFlight] = useState<ImportFlightState | null>(
111
- null,
112
- );
113
- const [alertState, setAlertState] = useState<AlertState | null>(null);
85
+ const [importFlight, setImportFlight] = useState<ImportFlightState | null>(null);
86
+ const confirm = useConfirmDialog();
114
87
  const fileInputRef = useRef<HTMLInputElement | null>(null);
115
88
  const selectedTargetRef = useRef<StorageTarget | null>(null);
116
89
  const discoveryRequestIdRef = useRef(0);
@@ -127,8 +100,7 @@ function StoragePanelContent() {
127
100
  const queryClient = useQueryClient();
128
101
  const debouncedSearch = useDebouncedValue(searchTerm);
129
102
  const selectedDescriptor = descriptors.find(
130
- (descriptor) =>
131
- selectedTarget && sameTarget(descriptor.target, selectedTarget),
103
+ (descriptor) => selectedTarget && sameTarget(descriptor.target, selectedTarget),
132
104
  );
133
105
  const previews = useStorageEntryPreviews({
134
106
  client,
@@ -146,10 +118,7 @@ function StoragePanelContent() {
146
118
  const supportedTypes = selectedDescriptor?.capabilities.supportedTypes ?? [];
147
119
  const sidebarGroups = useMemo(() => {
148
120
  const storages = new Map(
149
- descriptors.map((descriptor) => [
150
- getStorageViewId(descriptor.target),
151
- descriptor,
152
- ]),
121
+ descriptors.map((descriptor) => [getStorageViewId(descriptor.target), descriptor]),
153
122
  );
154
123
  return buildStorageSidebarGroups(storages);
155
124
  }, [descriptors]);
@@ -169,17 +138,21 @@ function StoragePanelContent() {
169
138
  useEffect(() => {
170
139
  if (!client) return;
171
140
 
141
+ const requestDiscovery = () => {
142
+ discoveryRequestIdRef.current += 1;
143
+ client.send('discover-storages', {
144
+ type: 'discover-storages',
145
+ requestId: `discovery-${discoveryRequestIdRef.current}`,
146
+ });
147
+ };
148
+
172
149
  const descriptorsSubscription = client.onMessage(
173
150
  'storage-descriptors',
174
151
  (event: StorageDiscoverStoragesResponseEvent) => {
175
- if (event.requestId !== `discovery-${discoveryRequestIdRef.current}`)
176
- return;
152
+ if (event.requestId !== `discovery-${discoveryRequestIdRef.current}`) return;
177
153
  setDescriptors(event.storages);
178
154
  setSelectedTarget((previous) =>
179
- previous &&
180
- event.storages.some((descriptor) =>
181
- sameTarget(descriptor.target, previous),
182
- )
155
+ previous && event.storages.some((descriptor) => sameTarget(descriptor.target, previous))
183
156
  ? previous
184
157
  : (event.storages[0]?.target ?? null),
185
158
  );
@@ -234,23 +207,16 @@ function StoragePanelContent() {
234
207
  'storage-invalidated',
235
208
  (event: StorageInvalidatedEvent) => {
236
209
  void dropStorageFullEntries(queryClient, event.target, event.key);
237
- if (event.operation === 'purge') {
238
- setDescriptors((current) =>
239
- current.map((descriptor) =>
240
- sameTarget(descriptor.target, event.target)
241
- ? { ...descriptor, entryCount: 0 }
242
- : descriptor,
243
- ),
244
- );
245
- }
246
- if (
247
- selectedTargetRef.current &&
248
- sameTarget(event.target, selectedTargetRef.current)
249
- ) {
210
+ setDescriptors((current) =>
211
+ current.map((descriptor) =>
212
+ sameTarget(descriptor.target, event.target)
213
+ ? { ...descriptor, entryCount: event.entryCount }
214
+ : descriptor,
215
+ ),
216
+ );
217
+ if (selectedTargetRef.current && sameTarget(event.target, selectedTargetRef.current)) {
250
218
  setInteraction((current) =>
251
- current && (event.key == null || current.key === event.key)
252
- ? null
253
- : current,
219
+ current && (event.key == null || current.key === event.key) ? null : current,
254
220
  );
255
221
  }
256
222
 
@@ -259,10 +225,7 @@ function StoragePanelContent() {
259
225
  pendingInvalidationsRef.current.add(targetId);
260
226
  queueMicrotask(() => {
261
227
  pendingInvalidationsRef.current.delete(targetId);
262
- if (
263
- selectedTargetRef.current &&
264
- sameTarget(event.target, selectedTargetRef.current)
265
- ) {
228
+ if (selectedTargetRef.current && sameTarget(event.target, selectedTargetRef.current)) {
266
229
  setVirtualListVersion((version) => version + 1);
267
230
  }
268
231
  void invalidateStorageEntryPreviews(queryClient, event.target);
@@ -270,16 +233,21 @@ function StoragePanelContent() {
270
233
  },
271
234
  );
272
235
 
273
- discoveryRequestIdRef.current += 1;
274
- client.send('discover-storages', {
275
- type: 'discover-storages',
276
- requestId: `discovery-${discoveryRequestIdRef.current}`,
236
+ // The device announces itself once it is listening. It reconnects on every
237
+ // app reload, so discovery has to run again — the descriptors and cached
238
+ // entries the panel holds belong to the previous JS context.
239
+ const deviceReadySubscription = client.onMessage('device-ready', () => {
240
+ void queryClient.resetQueries();
241
+ requestDiscovery();
277
242
  });
243
+
244
+ requestDiscovery();
278
245
  return () => {
279
246
  descriptorsSubscription.remove();
280
247
  importProgressSubscription.remove();
281
248
  importResultSubscription.remove();
282
249
  invalidationSubscription.remove();
250
+ deviceReadySubscription.remove();
283
251
  };
284
252
  }, [client, queryClient]);
285
253
 
@@ -291,8 +259,6 @@ function StoragePanelContent() {
291
259
  importPreviewAbortControllerRef.current = null;
292
260
  activeImportRequestIdRef.current = null;
293
261
  setExportState({ status: 'idle' });
294
- setDeleteKey(null);
295
- setShowPurgeDialog(false);
296
262
  setInteraction(null);
297
263
  setImportFlight(null);
298
264
  }, [selectedTarget]);
@@ -310,11 +276,7 @@ function StoragePanelContent() {
310
276
  };
311
277
 
312
278
  const handleValueChange = (key: string, newValue: StorageEntryValue) => {
313
- if (
314
- !client ||
315
- !selectedTarget ||
316
- !supportedTypes.includes(getEntryTypeFromValue(newValue))
317
- )
279
+ if (!client || !selectedTarget || !supportedTypes.includes(getEntryTypeFromValue(newValue)))
318
280
  return;
319
281
  const type = getEntryTypeFromValue(newValue);
320
282
  const entry =
@@ -345,30 +307,47 @@ function StoragePanelContent() {
345
307
  );
346
308
  };
347
309
 
348
- const handleDeleteEntry = () => {
349
- if (!client || !selectedTarget || !deleteKey) return;
350
- const key = deleteKey;
351
- setDeleteKey(null);
352
- mutateSelectedStorage(() =>
353
- client.send('delete-entry', {
354
- type: 'delete-entry',
355
- target: selectedTarget,
356
- key,
357
- }),
358
- );
359
- };
310
+ const handleDeleteClick = useCallback(
311
+ async (key: string) => {
312
+ if (!client || !selectedTarget) return;
313
+ const confirmed = await confirm({
314
+ title: 'Delete Entry',
315
+ description: `Are you sure you want to delete the entry "${key}"?`,
316
+ tone: 'danger',
317
+ confirmLabel: 'Delete',
318
+ });
319
+ if (!confirmed) return;
320
+ mutateSelectedStorage(() =>
321
+ client.send('delete-entry', {
322
+ type: 'delete-entry',
323
+ target: selectedTarget,
324
+ key,
325
+ }),
326
+ );
327
+ },
328
+ [client, selectedTarget, confirm],
329
+ );
360
330
 
361
- const handlePurgeStorage = () => {
331
+ const handlePurgeClick = async () => {
362
332
  if (!client || !selectedTarget) return;
363
- setShowPurgeDialog(false);
333
+ const confirmed = await confirm({
334
+ title: 'Purge Storage',
335
+ description: selectedDescriptor
336
+ ? `Are you sure you want to remove all entries from "${selectedDescriptor.storageName}"? This action cannot be undone.`
337
+ : 'Are you sure you want to remove all entries from this storage? This action cannot be undone.',
338
+ tone: 'danger',
339
+ confirmLabel: 'Purge',
340
+ });
341
+ if (!confirmed) return;
364
342
  client.send('purge-storage', {
365
343
  type: 'purge-storage',
366
344
  target: selectedTarget,
367
345
  });
368
346
  };
369
347
 
370
- const showAlert = (title: string, message: string) =>
371
- setAlertState({ title, message });
348
+ const showAlert = async (title: string, message: string) => {
349
+ await confirm({ variant: 'alert', title, description: message });
350
+ };
372
351
 
373
352
  const handleImportClick = () => {
374
353
  if (fileInputRef.current) {
@@ -384,18 +363,12 @@ function StoragePanelContent() {
384
363
  try {
385
364
  raw = JSON.parse(await file.text());
386
365
  } catch (error) {
387
- showAlert(
388
- 'Could not read file',
389
- error instanceof Error ? error.message : String(error),
390
- );
366
+ void showAlert('Could not read file', error instanceof Error ? error.message : String(error));
391
367
  return;
392
368
  }
393
369
  const parsed = parseSnapshot(raw);
394
370
  if (!parsed.ok) {
395
- showAlert(
396
- 'Invalid snapshot',
397
- `${parsed.error.path}: ${parsed.error.message}`,
398
- );
371
+ void showAlert('Invalid snapshot', `${parsed.error.path}: ${parsed.error.message}`);
399
372
  return;
400
373
  }
401
374
  importPreviewAbortControllerRef.current?.abort();
@@ -410,10 +383,7 @@ function StoragePanelContent() {
410
383
  payload: { type: 'preview-import', target, snapshot: parsed.snapshot },
411
384
  signal: controller.signal,
412
385
  });
413
- if (
414
- controller.signal.aborted ||
415
- !sameTarget(target, selectedTargetRef.current ?? target)
416
- )
386
+ if (controller.signal.aborted || !sameTarget(target, selectedTargetRef.current ?? target))
417
387
  return;
418
388
  setImportFlight({
419
389
  phase: 'preview',
@@ -428,11 +398,8 @@ function StoragePanelContent() {
428
398
  ),
429
399
  });
430
400
  } catch {
431
- if (
432
- !controller.signal.aborted &&
433
- sameTarget(target, selectedTargetRef.current ?? target)
434
- ) {
435
- showAlert(
401
+ if (!controller.signal.aborted && sameTarget(target, selectedTargetRef.current ?? target)) {
402
+ void showAlert(
436
403
  'Could not preview import',
437
404
  'Could not inspect the selected storage. Please try again.',
438
405
  );
@@ -476,26 +443,19 @@ function StoragePanelContent() {
476
443
  payload: { type: 'export-snapshot', target },
477
444
  signal: controller.signal,
478
445
  });
479
- if (
480
- !controller.signal.aborted &&
481
- sameTarget(target, selectedTargetRef.current ?? target)
482
- ) {
446
+ if (!controller.signal.aborted && sameTarget(target, selectedTargetRef.current ?? target)) {
483
447
  downloadJson(response.snapshot, buildExportFilename(target));
484
448
  setExportState({ status: 'idle' });
485
449
  }
486
450
  } catch {
487
- if (
488
- !controller.signal.aborted &&
489
- sameTarget(target, selectedTargetRef.current ?? target)
490
- ) {
451
+ if (!controller.signal.aborted && sameTarget(target, selectedTargetRef.current ?? target)) {
491
452
  setExportState({
492
453
  status: 'error',
493
454
  message: 'Could not export the selected storage. Please try again.',
494
455
  });
495
456
  }
496
457
  } finally {
497
- if (exportAbortControllerRef.current === controller)
498
- exportAbortControllerRef.current = null;
458
+ if (exportAbortControllerRef.current === controller) exportAbortControllerRef.current = null;
499
459
  }
500
460
  };
501
461
 
@@ -507,9 +467,7 @@ function StoragePanelContent() {
507
467
  header: 'Key',
508
468
  enableSorting: true,
509
469
  cell: ({ row }) => (
510
- <span className="font-mono text-sm text-foreground">
511
- {row.original.key}
512
- </span>
470
+ <span className="font-mono text-sm text-foreground">{row.original.key}</span>
513
471
  ),
514
472
  },
515
473
  {
@@ -517,7 +475,11 @@ function StoragePanelContent() {
517
475
  accessorKey: 'type',
518
476
  header: 'Type',
519
477
  enableSorting: false,
520
- cell: ({ row }) => <Badge variant="outline">{row.original.type}</Badge>,
478
+ cell: ({ row }) => (
479
+ <Badge tone="neutral" variant="outline">
480
+ {row.original.type}
481
+ </Badge>
482
+ ),
521
483
  },
522
484
  {
523
485
  id: 'preview',
@@ -532,9 +494,7 @@ function StoragePanelContent() {
532
494
  <span
533
495
  className="ml-2 text-xs text-muted-foreground"
534
496
  title={
535
- row.original.isTruncated
536
- ? `Full value size: ${row.original.valueSize}`
537
- : undefined
497
+ row.original.isTruncated ? `Full value size: ${row.original.valueSize}` : undefined
538
498
  }
539
499
  >
540
500
  {row.original.isTruncated
@@ -549,38 +509,31 @@ function StoragePanelContent() {
549
509
  header: '',
550
510
  enableSorting: false,
551
511
  cell: ({ row }) => (
552
- <div
553
- className="flex items-center gap-1"
554
- onClick={(event) => event.stopPropagation()}
555
- >
556
- <Button
512
+ <div className="flex items-center gap-1" onClick={(event) => event.stopPropagation()}>
513
+ <IconButton
514
+ tone="neutral"
557
515
  variant="ghost"
558
- size="icon"
559
- onClick={() =>
560
- setInteraction({ key: row.original.key, mode: 'edit' })
561
- }
562
- aria-label={`Edit value for ${row.original.key}`}
516
+ onClick={() => setInteraction({ key: row.original.key, mode: 'edit' })}
517
+ label={`Edit value for ${row.original.key}`}
563
518
  >
564
519
  <Edit3 className="h-3.5 w-3.5" />
565
- </Button>
566
- <Button
520
+ </IconButton>
521
+ <IconButton
522
+ tone="neutral"
567
523
  variant="ghost"
568
- size="icon"
569
- className="text-muted-foreground hover:text-destructive"
570
- onClick={() => setDeleteKey(row.original.key)}
571
- aria-label={`Delete entry ${row.original.key}`}
524
+ className="text-muted-foreground hover:text-danger"
525
+ onClick={() => void handleDeleteClick(row.original.key)}
526
+ label={`Delete entry ${row.original.key}`}
572
527
  >
573
528
  <Trash2 className="h-3.5 w-3.5" />
574
- </Button>
529
+ </IconButton>
575
530
  </div>
576
531
  ),
577
532
  },
578
533
  ],
579
- [],
534
+ [handleDeleteClick],
580
535
  );
581
- const sorting: SortingState = [
582
- { id: 'key', desc: keySortDirection === 'descending' },
583
- ];
536
+ const sorting: SortingState = [{ id: 'key', desc: keySortDirection === 'descending' }];
584
537
  const handleSortingChange: OnChangeFn<SortingState> = (updater) => {
585
538
  const next = typeof updater === 'function' ? updater(sorting) : updater;
586
539
  const keySort = next.find((item) => item.id === 'key');
@@ -612,12 +565,10 @@ function StoragePanelContent() {
612
565
  isFetchingPreviousPageRef.current = false;
613
566
  });
614
567
  };
615
- const selectedStorageViewId = selectedTarget
616
- ? getStorageViewId(selectedTarget)
617
- : '';
568
+ const selectedStorageViewId = selectedTarget ? getStorageViewId(selectedTarget) : '';
618
569
 
619
570
  return (
620
- <PluginShell>
571
+ <>
621
572
  <PluginShell.Body>
622
573
  <Split direction="horizontal" autoSaveId="storage">
623
574
  <Split.Pane defaultSize={22} minSize={15} maxSize={40}>
@@ -628,22 +579,16 @@ function StoragePanelContent() {
628
579
  </div>
629
580
  ) : (
630
581
  sidebarGroups.map((group) => (
631
- <Sidebar.Group
632
- key={group.adapterId}
633
- label={group.adapterName}
634
- >
582
+ <Sidebar.Group key={group.adapterId} label={group.adapterName}>
635
583
  {group.items.map((item) => (
636
584
  <Sidebar.Item
637
585
  key={item.viewId}
638
586
  selected={item.viewId === selectedStorageViewId}
639
- trailing={
640
- <Badge variant="secondary">{item.entryCount}</Badge>
641
- }
587
+ leading={<Database />}
588
+ trailing={<Badge tone="neutral">{item.entryCount}</Badge>}
642
589
  onClick={() => {
643
590
  const descriptor = descriptors.find(
644
- (candidate) =>
645
- getStorageViewId(candidate.target) ===
646
- item.viewId,
591
+ (candidate) => getStorageViewId(candidate.target) === item.viewId,
647
592
  );
648
593
  if (descriptor) setSelectedTarget(descriptor.target);
649
594
  }}
@@ -666,16 +611,16 @@ function StoragePanelContent() {
666
611
  disabled={!selectedDescriptor}
667
612
  aria-label="Add entry"
668
613
  title="Add entry"
669
- className="w-7 px-0"
614
+ className="w-6 px-0"
670
615
  >
671
616
  <Plus className="h-3.5 w-3.5" />
672
617
  </Toolbar.Button>
673
618
  <Toolbar.Button
674
- onClick={() => setShowPurgeDialog(true)}
619
+ onClick={() => void handlePurgeClick()}
675
620
  disabled={!client || !selectedTarget || previews.isFetching}
676
621
  aria-label="Purge storage"
677
622
  title="Purge storage"
678
- className="w-7 px-0 text-muted-foreground hover:text-destructive"
623
+ className="w-6 px-0 text-muted-foreground hover:text-danger"
679
624
  >
680
625
  <Trash2 className="h-3.5 w-3.5" />
681
626
  </Toolbar.Button>
@@ -687,7 +632,7 @@ function StoragePanelContent() {
687
632
  disabled={!selectedTarget || previews.isFetching}
688
633
  aria-label="Refresh storage"
689
634
  title="Refresh storage"
690
- className="w-7 px-0"
635
+ className="w-6 px-0"
691
636
  >
692
637
  <RefreshCw className="h-3.5 w-3.5" />
693
638
  </Toolbar.Button>
@@ -699,28 +644,20 @@ function StoragePanelContent() {
699
644
  disabled={!selectedDescriptor}
700
645
  aria-label="Import storage"
701
646
  title="Import storage"
702
- className="w-7 px-0"
647
+ className="w-6 px-0"
703
648
  >
704
649
  <Upload className="h-3.5 w-3.5" />
705
650
  </Toolbar.Button>
706
651
  <Toolbar.Button
707
652
  onClick={handleExport}
708
- disabled={
709
- !client ||
710
- !selectedTarget ||
711
- exportState.status === 'loading'
712
- }
653
+ disabled={!client || !selectedTarget || exportState.status === 'loading'}
713
654
  aria-label={
714
- exportState.status === 'loading'
715
- ? 'Exporting storage'
716
- : 'Export storage'
655
+ exportState.status === 'loading' ? 'Exporting storage' : 'Export storage'
717
656
  }
718
657
  title={
719
- exportState.status === 'loading'
720
- ? 'Exporting storage'
721
- : 'Export storage'
658
+ exportState.status === 'loading' ? 'Exporting storage' : 'Export storage'
722
659
  }
723
- className="w-7 px-0"
660
+ className="w-6 px-0"
724
661
  >
725
662
  <Download className="h-3.5 w-3.5" />
726
663
  </Toolbar.Button>
@@ -736,7 +673,7 @@ function StoragePanelContent() {
736
673
  />
737
674
  </div>
738
675
  {exportState.status === 'error' ? (
739
- <span role="alert" className="text-xs text-destructive">
676
+ <span role="alert" className="text-xs text-danger">
740
677
  {exportState.message}
741
678
  </span>
742
679
  ) : null}
@@ -759,15 +696,11 @@ function StoragePanelContent() {
759
696
  getRowTextValue={(entry) => entry.key}
760
697
  loading={previews.isLoading}
761
698
  emptyMessage={
762
- debouncedSearch
763
- ? 'No entries match your search.'
764
- : 'This storage is empty.'
699
+ debouncedSearch ? 'No entries match your search.' : 'This storage is empty.'
765
700
  }
766
701
  manualSorting
767
702
  onEndReached={fetchNextPage}
768
- onRowClick={(entry) =>
769
- setInteraction({ key: entry.key, mode: 'detail' })
770
- }
703
+ onRowClick={(entry) => setInteraction({ key: entry.key, mode: 'detail' })}
771
704
  onSortingChange={handleSortingChange}
772
705
  onStartReached={fetchPreviousPage}
773
706
  scrollClassName="h-full w-full overflow-auto"
@@ -799,9 +732,7 @@ function StoragePanelContent() {
799
732
  if (!open) closeInteraction();
800
733
  }}
801
734
  onEdit={() =>
802
- setInteraction((current) =>
803
- current ? { ...current, mode: 'edit' } : current,
804
- )
735
+ setInteraction((current) => (current ? { ...current, mode: 'edit' } : current))
805
736
  }
806
737
  entry={fullEntry.entry ?? null}
807
738
  />
@@ -818,53 +749,16 @@ function StoragePanelContent() {
818
749
  onCancel={() => setImportFlight(null)}
819
750
  onClose={() => setImportFlight(null)}
820
751
  />
821
- <ConfirmDialog
822
- open={deleteKey !== null}
823
- onOpenChange={(open) => {
824
- if (!open) setDeleteKey(null);
825
- }}
826
- variant="confirm"
827
- destructive
828
- title="Delete Entry"
829
- description={
830
- deleteKey
831
- ? `Are you sure you want to delete the entry "${deleteKey}"?`
832
- : undefined
833
- }
834
- confirmLabel="Delete"
835
- onConfirm={handleDeleteEntry}
836
- />
837
- <ConfirmDialog
838
- open={showPurgeDialog}
839
- onOpenChange={setShowPurgeDialog}
840
- variant="confirm"
841
- destructive
842
- title="Purge Storage"
843
- description={
844
- selectedDescriptor
845
- ? `Are you sure you want to remove all entries from "${selectedDescriptor.storageName}"? This action cannot be undone.`
846
- : 'Are you sure you want to remove all entries from this storage? This action cannot be undone.'
847
- }
848
- confirmLabel="Purge"
849
- onConfirm={handlePurgeStorage}
850
- />
851
- <ConfirmDialog
852
- open={alertState !== null}
853
- onOpenChange={(open) => {
854
- if (!open) setAlertState(null);
855
- }}
856
- variant="alert"
857
- title={alertState?.title ?? ''}
858
- description={alertState?.message}
859
- />
860
- </PluginShell>
752
+ </>
861
753
  );
862
754
  }
863
755
 
864
756
  export default function StoragePanel() {
865
757
  return (
866
758
  <StorageQueryClientProvider>
867
- <StoragePanelContent />
759
+ <PluginShell>
760
+ <StoragePanelContent />
761
+ </PluginShell>
868
762
  </StorageQueryClientProvider>
869
763
  );
870
764
  }