From a89838af1b7a7c23ae4930d972dd830cb7cd8d6c Mon Sep 17 00:00:00 2001 From: Anatoly Belobrovik Date: Fri, 24 Apr 2026 17:43:32 +0200 Subject: [PATCH 1/3] feat: share storage inspector UI Add the shared @rozenite/ui package with HeroUI-backed primitives. Move JSON inspection into shared UI and reuse it from Storage and MMKV. Migrate both storage inspector panels while preserving existing protocols. --- packages/mmkv-plugin/package.json | 6 +- packages/mmkv-plugin/postcss.config.js | 3 +- .../mmkv-plugin/src/ui/add-entry-dialog.tsx | 400 +++++---- .../mmkv-plugin/src/ui/confirm-dialog.tsx | 123 ++- .../mmkv-plugin/src/ui/edit-entry-dialog.tsx | 414 ++++----- .../mmkv-plugin/src/ui/editable-table.tsx | 422 ++++----- .../src/ui/entry-detail-dialog.tsx | 323 ++++--- packages/mmkv-plugin/src/ui/globals.css | 86 +- packages/mmkv-plugin/src/ui/panel.tsx | 486 +++++------ packages/mmkv-plugin/tailwind.config.ts | 94 -- packages/mmkv-plugin/tsconfig.json | 6 + packages/storage-plugin/package.json | 6 +- packages/storage-plugin/postcss.config.js | 3 +- .../src/ui/add-entry-dialog.tsx | 392 +++++---- .../storage-plugin/src/ui/confirm-dialog.tsx | 123 ++- .../src/ui/edit-entry-dialog.tsx | 380 ++++---- .../storage-plugin/src/ui/editable-table.tsx | 329 ++++--- .../src/ui/entry-detail-dialog.tsx | 333 ++++---- packages/storage-plugin/src/ui/globals.css | 86 +- packages/storage-plugin/src/ui/panel.tsx | 288 +++---- packages/storage-plugin/tailwind.config.ts | 94 -- packages/storage-plugin/tsconfig.json | 4 +- packages/ui/package.json | 76 ++ packages/ui/src/hooks/useCopyToClipboard.ts | 54 ++ packages/ui/src/index.ts | 37 + packages/ui/src/json-inspector.tsx | 99 +++ packages/ui/src/utils/cn.ts | 2 + packages/ui/src/utils/json.ts | 38 + packages/ui/styles.css | 97 +++ packages/ui/tsconfig.json | 10 + packages/ui/tsconfig.lib.json | 20 + packages/ui/vite.config.ts | 41 + pnpm-lock.yaml | 808 +++++------------- tsconfig.base.json | 5 + 34 files changed, 2800 insertions(+), 2888 deletions(-) delete mode 100644 packages/mmkv-plugin/tailwind.config.ts delete mode 100644 packages/storage-plugin/tailwind.config.ts create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/hooks/useCopyToClipboard.ts create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/src/json-inspector.tsx create mode 100644 packages/ui/src/utils/cn.ts create mode 100644 packages/ui/src/utils/json.ts create mode 100644 packages/ui/styles.css create mode 100644 packages/ui/tsconfig.json create mode 100644 packages/ui/tsconfig.lib.json create mode 100644 packages/ui/vite.config.ts diff --git a/packages/mmkv-plugin/package.json b/packages/mmkv-plugin/package.json index 056a2392..707d90f4 100644 --- a/packages/mmkv-plugin/package.json +++ b/packages/mmkv-plugin/package.json @@ -23,9 +23,11 @@ "dependencies": { "@rozenite/agent-bridge": "workspace:*", "@rozenite/plugin-bridge": "workspace:*", + "@rozenite/ui": "workspace:*", "nanoevents": "^9.1.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.2.2", "@rozenite/vite-plugin": "workspace:*", "@tanstack/react-table": "^8.21.3", "@types/react": "catalog:", @@ -34,7 +36,6 @@ "postcss": "^8.5.6", "react": "catalog:", "react-dom": "catalog:", - "react-json-tree": "^0.20.0", "react-native": "catalog:", "react-native-mmkv": "^3.3.0", "react-native-mmkv-v3": "npm:react-native-mmkv@^3.0.0", @@ -42,8 +43,9 @@ "react-native-nitro-modules": "*", "react-native-web": "^0.21.2", "rozenite": "workspace:*", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", + "tailwind-variants": "^2.0.0", "typescript": "~5.9.3", "vite": "catalog:" }, diff --git a/packages/mmkv-plugin/postcss.config.js b/packages/mmkv-plugin/postcss.config.js index 2aa7205d..a34a3d56 100644 --- a/packages/mmkv-plugin/postcss.config.js +++ b/packages/mmkv-plugin/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/packages/mmkv-plugin/src/ui/add-entry-dialog.tsx b/packages/mmkv-plugin/src/ui/add-entry-dialog.tsx index 7260bf43..8116925b 100644 --- a/packages/mmkv-plugin/src/ui/add-entry-dialog.tsx +++ b/packages/mmkv-plugin/src/ui/add-entry-dialog.tsx @@ -1,54 +1,85 @@ import { useState } from 'react'; -import { X } from 'lucide-react'; -import { MMKVEntry, MMKVEntryType, MMKVEntryValue } from '../shared/types'; +import { + Button, + Description, + Input, + Label, + ListBox, + Modal, + Select, + TextField, +} from '@rozenite/ui'; +import { Plus } from 'lucide-react'; +import type { MMKVEntry, MMKVEntryType, MMKVEntryValue } from '../shared/types'; import { ConfirmDialog } from './confirm-dialog'; -export type AddEntryDialogProps = { +const TYPE_OPTIONS: Array<{ value: MMKVEntryType; label: string }> = [ + { value: 'string', label: 'String' }, + { value: 'number', label: 'Number' }, + { value: 'boolean', label: 'Boolean' }, + { value: 'buffer', label: 'Buffer (Array)' }, +]; + +type DialogState = { isOpen: boolean; - onClose: () => void; + title: string; + message: string; + type: 'confirm' | 'alert'; + onConfirm?: () => void; +}; + +const EMPTY_DIALOG_STATE: DialogState = { + isOpen: false, + title: '', + message: '', + type: 'alert', +}; + +export type AddEntryDialogProps = { onAddEntry: (entry: MMKVEntry) => void; existingKeys: string[]; + isDisabled?: boolean; }; export const AddEntryDialog = ({ - isOpen, - onClose, onAddEntry, existingKeys, + isDisabled = false, }: AddEntryDialogProps) => { + const [isOpen, setIsOpen] = useState(false); const [newEntryKey, setNewEntryKey] = useState(''); const [newEntryType, setNewEntryType] = useState('string'); const [newEntryValue, setNewEntryValue] = useState(''); - const [confirmDialog, setConfirmDialog] = useState<{ - isOpen: boolean; - title: string; - message: string; - type: 'confirm' | 'alert'; - onConfirm?: () => void; - }>({ isOpen: false, title: '', message: '', type: 'alert' }); + const [confirmDialog, setConfirmDialog] = + useState(EMPTY_DIALOG_STATE); const resetForm = () => { setNewEntryKey(''); setNewEntryType('string'); setNewEntryValue(''); - onClose(); + setConfirmDialog(EMPTY_DIALOG_STATE); + }; + + const closeDialog = () => { + resetForm(); + setIsOpen(false); }; const handleAddEntry = () => { - if (!newEntryKey.trim()) return; + if (!newEntryKey.trim()) { + return; + } - // Check if key already exists if (existingKeys.includes(newEntryKey)) { setConfirmDialog({ isOpen: true, title: 'Key Already Exists', - message: 'An entry with this key already exists!', + message: 'An entry with this key already exists.', type: 'alert', }); return; } - // Parse the value based on type let parsedValue: MMKVEntryValue; try { switch (newEntryType) { @@ -57,18 +88,21 @@ export const AddEntryDialog = ({ break; case 'number': parsedValue = Number(newEntryValue); - if (isNaN(parsedValue as number)) { + if (Number.isNaN(parsedValue)) { throw new Error('Invalid number'); } break; case 'boolean': - parsedValue = newEntryValue.toLowerCase() === 'true'; + if (newEntryValue !== 'true' && newEntryValue !== 'false') { + throw new Error('Boolean value must be true or false'); + } + parsedValue = newEntryValue === 'true'; break; case 'buffer': parsedValue = JSON.parse(newEntryValue); if ( !Array.isArray(parsedValue) || - !parsedValue.every((v) => typeof v === 'number') + !parsedValue.every((value) => typeof value === 'number') ) { throw new Error('Buffer must be an array of numbers'); } @@ -88,172 +122,206 @@ export const AddEntryDialog = ({ return; } - const newEntry: MMKVEntry = { - key: newEntryKey, - type: newEntryType, - value: parsedValue, - } as MMKVEntry; - - onAddEntry(newEntry); + let entry: MMKVEntry; + if (newEntryType === 'string') { + entry = { + key: newEntryKey, + type: 'string', + value: parsedValue as string, + }; + } else if (newEntryType === 'number') { + entry = { + key: newEntryKey, + type: 'number', + value: parsedValue as number, + }; + } else if (newEntryType === 'boolean') { + entry = { + key: newEntryKey, + type: 'boolean', + value: parsedValue as boolean, + }; + } else { + entry = { + key: newEntryKey, + type: 'buffer', + value: parsedValue as number[], + }; + } - // Reset form and close dialog - resetForm(); + onAddEntry(entry); + closeDialog(); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - resetForm(); - } else if ( - e.key === 'Enter' && - newEntryKey.trim() && - newEntryValue.trim() - ) { + const handleInputKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && newEntryKey.trim() && newEntryValue.trim()) { handleAddEntry(); } }; - if (!isOpen) return null; - return ( -
-
e.stopPropagation()} - onKeyDown={handleKeyDown} - > -
-

Add New Entry

- -
+ <> + -
- {/* Key Input */} -
- - setNewEntryKey(e.target.value)} - placeholder="Enter key name" - 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" - autoFocus - /> -
- - {/* Type Select */} -
- - -
+ { + setIsOpen(nextOpen); + if (!nextOpen) { + resetForm(); + } + }} + > + + + + + + Add New Entry + + + + + setNewEntryKey(event.target.value)} + onKeyDown={handleInputKeyDown} + placeholder="Enter key name" + value={newEntryKey} + variant="secondary" + /> + - {/* Value Input */} -
- - {newEntryType === 'boolean' ? ( - - ) : ( - setNewEntryValue(e.target.value)} - placeholder={ - newEntryType === 'string' - ? 'Enter string value' - : newEntryType === 'number' - ? 'Enter number value' - : newEntryType === 'buffer' - ? 'Enter array as JSON, e.g., [1, 2, 3]' - : 'Enter value' - } - 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" - /> - )} - {newEntryType === 'buffer' && ( -

- Enter as JSON array of numbers, e.g., [1, 2, 3, 255] -

- )} -
-
+
+ + +
- {/* Dialog Actions */} -
- - -
-
+ {newEntryType === 'boolean' ? ( +
+ + +
+ ) : ( + + + setNewEntryValue(event.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={ + newEntryType === 'string' + ? 'Enter string value' + : newEntryType === 'number' + ? 'Enter number value' + : newEntryType === 'buffer' + ? 'Enter array as JSON, e.g., [1, 2, 3]' + : 'Enter value' + } + value={newEntryValue} + variant="secondary" + /> + {newEntryType === 'buffer' ? ( + + Enter as JSON array of numbers, e.g., [1, 2, 3, 255] + + ) : null} + + )} + + + + + + + + + setConfirmDialog((prev) => ({ ...prev, isOpen: false }))} + message={confirmDialog.message} + onClose={() => + setConfirmDialog((previous) => ({ ...previous, isOpen: false })) + } onConfirm={() => { if (confirmDialog.onConfirm) { confirmDialog.onConfirm(); } }} title={confirmDialog.title} - message={confirmDialog.message} type={confirmDialog.type} /> -
+ ); }; diff --git a/packages/mmkv-plugin/src/ui/confirm-dialog.tsx b/packages/mmkv-plugin/src/ui/confirm-dialog.tsx index fe024a34..4c7a25dc 100644 --- a/packages/mmkv-plugin/src/ui/confirm-dialog.tsx +++ b/packages/mmkv-plugin/src/ui/confirm-dialog.tsx @@ -1,4 +1,5 @@ -import { X, AlertTriangle, Info } from 'lucide-react'; +import { AlertDialog, Button } from '@rozenite/ui'; +import { AlertTriangle, Info } from 'lucide-react'; export type ConfirmDialogProps = { isOpen: boolean; @@ -21,80 +22,60 @@ export const ConfirmDialog = ({ confirmText = 'Confirm', cancelText = 'Cancel', }: ConfirmDialogProps) => { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } else if (e.key === 'Enter') { - onConfirm(); - } - }; - - const handleConfirm = () => { - onConfirm(); - onClose(); - }; - - const handleCancel = () => { - onClose(); - }; - - if (!isOpen) return null; + const isConfirm = type === 'confirm'; return ( -
{ + if (!nextOpen) { + onClose(); + } + }} > -
e.stopPropagation()} - onKeyDown={handleKeyDown} + -
-
- {type === 'confirm' ? ( - - ) : ( - - )} -

{title}

-
- -
- -
-

{message}

-
- - {/* Dialog Actions */} -
- {type === 'confirm' && ( - - )} - -
-
-
+ + + {isConfirm ? ( + + ) : ( + + )} + + + {title} + + + + {message} + + + {isConfirm ? ( + + ) : null} + + + + + + ); }; diff --git a/packages/mmkv-plugin/src/ui/edit-entry-dialog.tsx b/packages/mmkv-plugin/src/ui/edit-entry-dialog.tsx index fc2c7aec..2c8d3e61 100644 --- a/packages/mmkv-plugin/src/ui/edit-entry-dialog.tsx +++ b/packages/mmkv-plugin/src/ui/edit-entry-dialog.tsx @@ -1,52 +1,100 @@ -import { useState, useEffect } from 'react'; -import { X, Edit3 } from 'lucide-react'; -import { MMKVEntry, MMKVEntryType, MMKVEntryValue } from '../shared/types'; +import { useEffect, useState } from 'react'; +import { + Button, + Chip, + Description, + Input, + Label, + ListBox, + Modal, + Select, + Surface, + TextField, +} from '@rozenite/ui'; +import { Edit3 } from 'lucide-react'; +import type { MMKVEntry, MMKVEntryType, MMKVEntryValue } from '../shared/types'; import { ConfirmDialog } from './confirm-dialog'; export type EditEntryDialogProps = { - isOpen: boolean; onClose: () => void; onEditEntry: (key: string, newValue: MMKVEntryValue) => void; entry: MMKVEntry | null; }; +type DialogState = { + isOpen: boolean; + title: string; + message: string; + type: 'confirm' | 'alert'; + onConfirm?: () => void; +}; + +const EMPTY_DIALOG_STATE: DialogState = { + isOpen: false, + title: '', + message: '', + type: 'alert', +}; + +const typeColorMap: Record< + MMKVEntryType, + 'success' | 'warning' | 'accent' | 'default' +> = { + string: 'success', + number: 'default', + boolean: 'warning', + buffer: 'accent', +}; + +const getInputType = (type: MMKVEntryType) => + type === 'number' ? 'number' : 'text'; + +const getPlaceholder = (type: MMKVEntryType) => { + if (type === 'string') { + return 'Enter string value'; + } + + if (type === 'number') { + return 'Enter number value'; + } + + if (type === 'boolean') { + return 'Enter true or false'; + } + + return 'Enter array as JSON, e.g., [1, 2, 3]'; +}; + export const EditEntryDialog = ({ - isOpen, onClose, onEditEntry, entry, }: EditEntryDialogProps) => { const [editValue, setEditValue] = useState(''); - const [confirmDialog, setConfirmDialog] = useState<{ - isOpen: boolean; - title: string; - message: string; - type: 'confirm' | 'alert'; - onConfirm?: () => void; - }>({ isOpen: false, title: '', message: '', type: 'alert' }); + const [confirmDialog, setConfirmDialog] = + useState(EMPTY_DIALOG_STATE); - // Reset form when entry changes or dialog opens useEffect(() => { - if (entry && isOpen) { - // Handle different value types for editing - let valueToEdit: string; - if (Array.isArray(entry.value)) { - // For buffer values, show as JSON - valueToEdit = JSON.stringify(entry.value); - } else { - valueToEdit = String(entry.value); - } - setEditValue(valueToEdit); + if (entry) { + setEditValue( + Array.isArray(entry.value) + ? JSON.stringify(entry.value) + : String(entry.value), + ); + setConfirmDialog(EMPTY_DIALOG_STATE); } - }, [entry, isOpen]); + }, [entry]); const resetForm = () => { setEditValue(''); + setConfirmDialog(EMPTY_DIALOG_STATE); onClose(); }; const handleEditEntry = () => { - if (!entry) return; + if (!entry) { + return; + } let newValue: MMKVEntryValue; @@ -57,32 +105,23 @@ export const EditEntryDialog = ({ break; case 'number': newValue = Number(editValue); - if (isNaN(newValue as number)) { + if (Number.isNaN(newValue)) { throw new Error('Invalid number'); } break; case 'boolean': - if ( - editValue.toLowerCase() !== 'true' && - editValue.toLowerCase() !== 'false' - ) { + if (editValue !== 'true' && editValue !== 'false') { throw new Error('Boolean value must be "true" or "false"'); } - newValue = editValue.toLowerCase() === 'true'; + newValue = editValue === 'true'; break; case 'buffer': - try { - newValue = JSON.parse(editValue); - if ( - !Array.isArray(newValue) || - !newValue.every((v) => typeof v === 'number') - ) { - throw new Error('Buffer must be an array of numbers'); - } - } catch { - throw new Error( - 'Invalid buffer format. Use JSON array like [1,2,3]' - ); + newValue = JSON.parse(editValue); + if ( + !Array.isArray(newValue) || + !newValue.every((value) => typeof value === 'number') + ) { + throw new Error('Buffer must be an array of numbers'); } break; default: @@ -104,182 +143,155 @@ export const EditEntryDialog = ({ resetForm(); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - resetForm(); - } else if (e.key === 'Enter' && editValue.trim()) { + const handleInputKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && editValue.trim()) { handleEditEntry(); } }; - const getInputType = (type: MMKVEntryType) => { - switch (type) { - case 'number': - return 'number'; - case 'boolean': - return 'text'; // We'll handle boolean conversion manually - default: - return 'text'; - } - }; - - const getPlaceholder = (type: MMKVEntryType) => { - switch (type) { - case 'string': - return 'Enter string value'; - case 'number': - return 'Enter number value'; - case 'boolean': - return 'Enter "true" or "false"'; - case 'buffer': - return 'Enter array as JSON, e.g., [1, 2, 3]'; - default: - return 'Enter value'; - } - }; - - const getTypeColorClass = (type: MMKVEntryType) => { - switch (type) { - case 'string': - return 'bg-green-600'; - case 'number': - return 'bg-blue-600'; - case 'boolean': - return 'bg-yellow-600'; - case 'buffer': - return 'bg-purple-600'; - default: - return 'bg-gray-600'; - } - }; - - if (!isOpen || !entry) return null; - return ( -
-
e.stopPropagation()} - onKeyDown={handleKeyDown} + <> + { + if (!nextOpen) { + resetForm(); + } + }} > -
-
- -

Edit Entry

-
- -
+ + + + + + + + + Edit Entry + + + {entry ? ( + <> +
+ + + {entry.key} + + + Key cannot be changed during editing. + +
-
- {/* Key Display */} -
- -
- {entry.key} -
-

- Key cannot be changed during editing -

-
+
+ + + {entry.type} + + + Type cannot be changed during editing. + +
- {/* Type Display */} -
- -
- - {entry.type} - -
-

- Type cannot be changed during editing -

-
- - {/* Value Input */} -
- - {entry.type === 'boolean' ? ( - - ) : ( - setEditValue(e.target.value)} - placeholder={getPlaceholder(entry.type)} - 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" - autoFocus - /> - )} - {entry.type === 'buffer' && ( -

- Enter as JSON array of numbers, e.g., [1, 2, 3, 255] -

- )} -
-
- - {/* Dialog Actions */} -
- - -
-
+ {entry.type === 'boolean' ? ( +
+ + +
+ ) : ( + + + setEditValue(event.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={getPlaceholder(entry.type)} + value={editValue} + variant="secondary" + /> + {entry.type === 'buffer' ? ( + + Enter as JSON array of numbers, e.g., [1, 2, 3, 255] + + ) : null} + + )} + + ) : null} + + + + + + + + + setConfirmDialog((prev) => ({ ...prev, isOpen: false }))} + message={confirmDialog.message} + onClose={() => + setConfirmDialog((previous) => ({ ...previous, isOpen: false })) + } onConfirm={() => { if (confirmDialog.onConfirm) { confirmDialog.onConfirm(); } }} title={confirmDialog.title} - message={confirmDialog.message} type={confirmDialog.type} /> -
+ ); }; diff --git a/packages/mmkv-plugin/src/ui/editable-table.tsx b/packages/mmkv-plugin/src/ui/editable-table.tsx index d05ace6c..81692557 100644 --- a/packages/mmkv-plugin/src/ui/editable-table.tsx +++ b/packages/mmkv-plugin/src/ui/editable-table.tsx @@ -1,18 +1,18 @@ -import { useState, useMemo } from 'react'; +import { useMemo, useState } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; import { - useReactTable, + createColumnHelper, + flexRender, getCoreRowModel, - getFilteredRowModel, getSortedRowModel, - flexRender, - createColumnHelper, - type ColumnDef, type SortingState, + useReactTable, } from '@tanstack/react-table'; -import { Trash2, Loader2, Edit3 } from 'lucide-react'; -import { MMKVEntry, MMKVEntryType, MMKVEntryValue } from '../shared/types'; -import { EditEntryDialog } from './edit-entry-dialog'; +import { Button, Chip, Table } from '@rozenite/ui'; +import { ChevronUp, Edit3, Inbox, Loader2, Trash2 } from 'lucide-react'; +import type { MMKVEntry, MMKVEntryType, MMKVEntryValue } from '../shared/types'; import { ConfirmDialog } from './confirm-dialog'; +import { EditEntryDialog } from './edit-entry-dialog'; export type EditableTableProps = { data: MMKVEntry[]; @@ -20,9 +20,68 @@ export type EditableTableProps = { onDeleteEntry?: (key: string) => void; onRowClick?: (entry: MMKVEntry) => void; loading?: boolean; + searchTerm?: string; +}; + +const typeColorMap: Record< + MMKVEntryType, + 'success' | 'danger' | 'warning' | 'accent' | 'default' +> = { + string: 'success', + number: 'default', + boolean: 'warning', + buffer: 'accent', }; const columnHelper = createColumnHelper(); +type TableSortDescriptor = NonNullable< + ComponentProps['sortDescriptor'] +>; + +function toSortDescriptor( + sorting: SortingState, +): TableSortDescriptor | undefined { + const firstSort = sorting[0]; + + if (!firstSort) { + return undefined; + } + + return { + column: firstSort.id, + direction: firstSort.desc ? 'descending' : 'ascending', + }; +} + +function toSortingState(descriptor: TableSortDescriptor): SortingState { + return [ + { + desc: descriptor.direction === 'descending', + id: String(descriptor.column), + }, + ]; +} + +function SortableColumnHeader({ + children, + sortDirection, +}: { + children: ReactNode; + sortDirection?: 'ascending' | 'descending'; +}) { + return ( + + {children} + {sortDirection ? ( + + ) : null} + + ); +} export const EditableTable = ({ data, @@ -30,112 +89,103 @@ export const EditableTable = ({ onDeleteEntry, onRowClick, loading = false, + searchTerm = '', }: EditableTableProps) => { const [editingEntry, setEditingEntry] = useState(null); - const [showEditDialog, setShowEditDialog] = useState(false); const [sorting, setSorting] = useState([]); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; entryKey: string; }>({ isOpen: false, entryKey: '' }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const columns = useMemo[]>( + const columns = useMemo( () => [ columnHelper.accessor('key', { header: 'Key', enableSorting: true, cell: ({ getValue }) => ( -
{getValue()}
+ + {getValue()} + ), }), columnHelper.accessor('type', { header: 'Type', enableSorting: true, - cell: ({ getValue }) => { - const type = getValue() as MMKVEntryType; + cell: (info) => { + const type = info.getValue() as MMKVEntryType; + return ( -
- - {type} - -
+ + {type} + ); }, }), columnHelper.accessor('value', { header: 'Value', - cell: ({ row }) => { - const entry = row.original; - return ( -
-
{formatValue(entry)}
- -
- ); - }, + cell: ({ row }) => ( +
{formatValue(row.original)}
+ ), }), columnHelper.display({ id: 'actions', header: 'Actions', cell: ({ row }) => ( -
- + + +
), }), ], - [onDeleteEntry] + [onDeleteEntry, onValueChange], ); const table = useReactTable({ data, columns, - state: { - sorting, - }, + state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), }); + const sortDescriptor = useMemo(() => toSortDescriptor(sorting), [sorting]); const handleEdit = (entry: MMKVEntry) => { setEditingEntry(entry); - setShowEditDialog(true); }; const handleEditEntry = (key: string, newValue: MMKVEntryValue) => { if (onValueChange) { onValueChange(key, newValue); } + setEditingEntry(null); - setShowEditDialog(false); }; const handleCloseEditDialog = () => { setEditingEntry(null); - setShowEditDialog(false); }; const handleDelete = (key: string) => { @@ -148,161 +198,114 @@ export const EditableTable = ({ if (onDeleteEntry && deleteConfirm.entryKey) { onDeleteEntry(deleteConfirm.entryKey); } - setDeleteConfirm({ isOpen: false, entryKey: '' }); - }; - - const getTypeColorClass = (type: string) => { - switch (type) { - case 'string': - return 'bg-green-600'; - case 'number': - return 'bg-blue-600'; - case 'boolean': - return 'bg-yellow-600'; - case 'buffer': - return 'bg-purple-600'; - default: - return 'bg-gray-600'; - } - }; - - const getTypeIcon = (type: string) => { - switch (type) { - case 'string': - return '📝'; - case 'number': - return '🔢'; - case 'boolean': - return '✅'; - case 'buffer': - return '💾'; - default: - return '❓'; - } - }; - const formatValue = (entry: MMKVEntry) => { - switch (entry.type) { - case 'string': - return ( - - "{entry.value as string}" - - ); - case 'number': - return ( - - {entry.value as number} - - ); - case 'boolean': - return ( - - {entry.value ? 'true' : 'false'} - - ); - case 'buffer': { - const bufferArray = entry.value as number[]; - const displayValue = - bufferArray.length > 5 - ? `[${bufferArray.slice(0, 5).join(', ')}, ...${ - bufferArray.length - 5 - } more]` - : `[${bufferArray.join(', ')}]`; - return ( - {displayValue} - ); - } - default: - return Unknown; - } + setDeleteConfirm({ isOpen: false, entryKey: '' }); }; if (loading) { return ( -
- -

Loading entries...

+
+ +

Loading entries...

); } return ( <> - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + + ) : ( + flexRender( + header.column.columnDef.header, + header.getContext(), + ) + ) + } + ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - { - // Don't trigger row click if clicking on buttons or interactive elements - const target = e.target as HTMLElement; - if ( - target.tagName === 'BUTTON' || - target.closest('button') || - target.tagName === 'INPUT' || - target.closest('input') - ) { - return; - } - if (onRowClick) { - onRowClick(row.original); - } - }} + + ( +
+ + + {searchTerm + ? 'No results found' + : 'This instance appears to be empty'} + +
+ )} > - {row.getVisibleCells().map((cell) => ( -
+ {table.getRowModel().rows.map((row) => ( + { + const target = event.target as HTMLElement; + if ( + target.tagName === 'BUTTON' || + target.closest('button') || + target.tagName === 'INPUT' || + target.closest('input') + ) { + return; + } + + onRowClick?.(row.original); + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + ))} - - ))} - -
+ + + setSorting(toSortingState(descriptor)) + } + sortDescriptor={sortDescriptor} + > + + {table.getHeaderGroups()[0]?.headers.map((header) => ( + -
- {header.isPlaceholder - ? null - : flexRender( + {({ sortDirection }) => + header.column.getCanSort() ? ( + + {flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} - {header.column.getCanSort() && ( - - {{ - asc: '↑', - desc: '↓', - }[header.column.getIsSorted() as string] ?? '↕'} - - )} -
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ + + + ); }; + +const formatValue = (entry: MMKVEntry) => { + if (entry.type === 'string') { + return ( + "{entry.value}" + ); + } + + if (entry.type === 'number') { + return ( + {entry.value} + ); + } + + if (entry.type === 'boolean') { + return ( + + {entry.value ? 'true' : 'false'} + + ); + } + + const displayValue = + entry.value.length > 5 + ? `[${entry.value.slice(0, 5).join(', ')}, ...${ + entry.value.length - 5 + } more]` + : `[${entry.value.join(', ')}]`; + + return {displayValue}; +}; diff --git a/packages/mmkv-plugin/src/ui/entry-detail-dialog.tsx b/packages/mmkv-plugin/src/ui/entry-detail-dialog.tsx index 45a7cb56..e7e70d4f 100644 --- a/packages/mmkv-plugin/src/ui/entry-detail-dialog.tsx +++ b/packages/mmkv-plugin/src/ui/entry-detail-dialog.tsx @@ -1,210 +1,175 @@ -import { X, Info, Edit3 } from 'lucide-react'; -import { JSONTree } from 'react-json-tree'; -import { MMKVEntry } from '../shared/types'; import { useMemo } from 'react'; +import { + Button, + Chip, + Description, + JsonInspector, + Label, + Modal, + parseJsonForInspection, + Surface, +} from '@rozenite/ui'; +import { Edit3, Info } from 'lucide-react'; +import type { MMKVEntry, MMKVEntryType } from '../shared/types'; export type EntryDetailDialogProps = { - isOpen: boolean; onClose: () => void; onEdit?: (entry: MMKVEntry) => void; entry: MMKVEntry | null; }; -const jsonTreeTheme = { - base00: 'transparent', - base01: '#374151', // bg-gray-700 - base02: '#4b5563', // bg-gray-600 - base03: '#6b7280', // text-gray-500 - base04: '#9ca3af', // text-gray-400 - base05: '#d1d5db', // text-gray-300 - base06: '#e5e7eb', // text-gray-200 - base07: '#f9fafb', // text-gray-100 - base08: '#ef4444', // text-red-500 - base09: '#f59e0b', // text-yellow-500 - base0A: '#10b981', // text-green-500 - base0B: '#3b82f6', // text-blue-500 - base0C: '#06b6d4', // text-cyan-500 - base0D: '#8b5cf6', // text-purple-500 - base0E: '#ec4899', // text-pink-500 - base0F: '#f97316', // text-orange-500 +const typeColorMap: Record< + MMKVEntryType, + 'success' | 'warning' | 'accent' | 'default' +> = { + string: 'success', + number: 'default', + boolean: 'warning', + buffer: 'accent', }; -const jsonSafeParse = (value: string): unknown | null => { - try { - return JSON.parse(value); - } catch { - return null; +const formatValue = (entry: MMKVEntry) => { + if (entry.type === 'string') { + return ( + + "{entry.value}" + + ); } -}; -const getTypeColorClass = (type: string) => { - switch (type) { - case 'string': - return 'bg-green-600'; - case 'number': - return 'bg-blue-600'; - case 'boolean': - return 'bg-yellow-600'; - case 'buffer': - return 'bg-purple-600'; - default: - return 'bg-gray-600'; + if (entry.type === 'number') { + return ( + {entry.value} + ); } -}; -const formatValue = (entry: MMKVEntry) => { - switch (entry.type) { - case 'string': - return ( - - "{entry.value as string}" - - ); - case 'number': - return ( - {entry.value as number} - ); - case 'boolean': - return ( - - {entry.value ? 'true' : 'false'} - - ); - case 'buffer': { - const bufferArray = entry.value as number[]; - return ( - - [{bufferArray.join(', ')}] - - ); - } - default: - return Unknown; + if (entry.type === 'boolean') { + return ( + + {entry.value ? 'true' : 'false'} + + ); } + + return ( + + [{entry.value.join(', ')}] + + ); }; export const EntryDetailDialog = ({ - isOpen, onClose, onEdit, entry, }: EntryDetailDialogProps) => { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - - if (!isOpen || !entry) return null; - - const isStringValue = entry.type === 'string'; - const stringValue = entry.value as string; - const jsonValue = useMemo( - () => isStringValue && jsonSafeParse(stringValue), - [isStringValue, stringValue] + const isStringValue = entry?.type === 'string'; + const stringValue = isStringValue ? entry.value : ''; + const jsonParseResult = useMemo( + () => + isStringValue + ? parseJsonForInspection(stringValue, 'any') + : { ok: false as const, value: null }, + [isStringValue, stringValue], ); + const shouldInspectJson = + jsonParseResult.ok && Boolean(jsonParseResult.value); return ( -
{ + if (!nextOpen) { + onClose(); + } + }} > -
e.stopPropagation()} - onKeyDown={handleKeyDown} - > -
-
- -

- Entry Details -

-
- -
+ + + + + + Entry Details + + + {entry ? ( + <> +
+ + + {entry.key} + +
-
- {/* Key Display */} -
- -
- {entry.key} -
-
+
+ + + {entry.type} + +
- {/* Type Display */} -
- -
- - {entry.type} - -
-
- - {/* Value Display */} -
- -
- {jsonValue ? ( - keyPath.length <= 2} - /> - ) : ( -
{formatValue(entry)}
- )} -
-
-
- - {/* Dialog Actions */} -
- {onEdit && ( - - )} - -
-
-
+
+ + + {shouldInspectJson ? ( + + keyPath.length <= 2 + } + theme="dark" + /> + ) : ( +
+ {formatValue(entry)} +
+ )} +
+ {entry.type === 'string' && shouldInspectJson ? ( + + Displaying parsed JSON from the stored string value. + + ) : null} +
+ + ) : null} + + + {entry && onEdit ? ( + + ) : null} + + + + + + ); }; diff --git a/packages/mmkv-plugin/src/ui/globals.css b/packages/mmkv-plugin/src/ui/globals.css index 2bf47da0..77390a0f 100644 --- a/packages/mmkv-plugin/src/ui/globals.css +++ b/packages/mmkv-plugin/src/ui/globals.css @@ -1,6 +1,5 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@import '@rozenite/ui/styles.css'; @layer utilities { .text-balance { @@ -12,87 +11,6 @@ } } -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - /* Custom scrollbar styles to match plugin theme */ ::-webkit-scrollbar { width: 8px; diff --git a/packages/mmkv-plugin/src/ui/panel.tsx b/packages/mmkv-plugin/src/ui/panel.tsx index 3582d9d7..375953d6 100644 --- a/packages/mmkv-plugin/src/ui/panel.tsx +++ b/packages/mmkv-plugin/src/ui/panel.tsx @@ -1,27 +1,44 @@ import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; -import { useEffect, useState } from 'react'; -import { Search, Plus } from 'lucide-react'; -import { MMKVEventMap } from '../shared/messaging'; -import { MMKVEntry, MMKVEntryValue } from '../shared/types'; +import { useEffect, useMemo, useState } from 'react'; +import { ListBox, SearchField, Select } from '@rozenite/ui'; +import type { + MMKVDeleteEntryEvent, + MMKVEventMap, + MMKVSetEntryEvent, + MMKVSnapshotEvent, +} from '../shared/messaging'; +import type { MMKVEntry, MMKVEntryValue } from '../shared/types'; import { EditableTable } from './editable-table'; import { AddEntryDialog } from './add-entry-dialog'; import { EntryDetailDialog } from './entry-detail-dialog'; import { EditEntryDialog } from './edit-entry-dialog'; import './globals.css'; +const getEntryTypeFromValue = (value: MMKVEntryValue): MMKVEntry['type'] => { + if (typeof value === 'string') { + return 'string'; + } + + if (typeof value === 'number') { + return 'number'; + } + + if (typeof value === 'boolean') { + return 'boolean'; + } + + return 'buffer'; +}; + export default function MMKVPanel() { const [instances, setInstances] = useState>( - new Map() + new Map(), ); const [selectedInstance, setSelectedInstance] = useState(null); - const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); - const [showAddDialog, setShowAddDialog] = useState(false); const [selectedEntry, setSelectedEntry] = useState(null); - const [showDetailDialog, setShowDetailDialog] = useState(false); const [editingEntry, setEditingEntry] = useState(null); - const [showEditDialog, setShowEditDialog] = useState(false); const client = useRozeniteDevToolsClient({ pluginId: '@rozenite/mmkv-plugin', @@ -32,92 +49,74 @@ export default function MMKVPanel() { return; } - const snapshotSubscription = client.onMessage('snapshot', (event) => { - setInstances((prevInstances) => { - const newInstances = new Map(prevInstances); - newInstances.set(event.id, event.entries); + const snapshotSubscription = client.onMessage( + 'snapshot', + (event: MMKVSnapshotEvent) => { + setInstances((previous) => { + const next = new Map(previous); + next.set(event.id, event.entries); - // If this is the first instance and no instance is selected, select it - if (prevInstances.size === 0 && !selectedInstance) { - setSelectedInstance(event.id); - } + if (previous.size === 0 && !selectedInstance) { + setSelectedInstance(event.id); + } - return newInstances; - }); + return next; + }); - // If this snapshot is for the currently selected instance, update entries - if (event.id === selectedInstance) { - setEntries(event.entries); - setLoading(false); - } - }); + if (event.id === selectedInstance) { + setLoading(false); + } + }, + ); - const setEntrySubscription = client.onMessage('set-entry', (event) => { - if (event.id === selectedInstance) { - setEntries((prevEntries) => { - const existingIndex = prevEntries.findIndex( - (entry) => entry.key === event.entry.key - ); - if (existingIndex >= 0) { - // Update existing entry - return prevEntries.map((entry) => - entry.key === event.entry.key ? event.entry : entry - ); - } else { - // Add new entry - return [...prevEntries, event.entry]; + const setEntrySubscription = client.onMessage( + 'set-entry', + (event: MMKVSetEntryEvent) => { + setInstances((previous) => { + const next = new Map(previous); + const current = next.get(event.id); + + if (!current) { + return previous; } - }); - } - // Update the instances map as well - setInstances((prevInstances) => { - const newInstances = new Map(prevInstances); - const instanceEntries = newInstances.get(event.id); - if (instanceEntries) { - const existingIndex = instanceEntries.findIndex( - (entry) => entry.key === event.entry.key + const existingIndex = current.findIndex( + (entry) => entry.key === event.entry.key, ); - if (existingIndex >= 0) { - // Update existing entry - const updatedEntries = instanceEntries.map((entry) => - entry.key === event.entry.key ? event.entry : entry - ); - newInstances.set(event.id, updatedEntries); - } else { - // Add new entry - newInstances.set(event.id, [...instanceEntries, event.entry]); - } - } - return newInstances; - }); - }); + + const entries = + existingIndex >= 0 + ? current.map((entry) => + entry.key === event.entry.key ? event.entry : entry, + ) + : [...current, event.entry]; + + next.set(event.id, entries); + return next; + }); + }, + ); const deleteEntrySubscription = client.onMessage( 'delete-entry', - (event) => { - if (event.id === selectedInstance) { - setEntries((prevEntries) => - prevEntries.filter((entry) => entry.key !== event.key) - ); - } + (event: MMKVDeleteEntryEvent) => { + setInstances((previous) => { + const next = new Map(previous); + const current = next.get(event.id); - // Update the instances map as well - setInstances((prevInstances) => { - const newInstances = new Map(prevInstances); - const instanceEntries = newInstances.get(event.id); - if (instanceEntries) { - const updatedEntries = instanceEntries.filter( - (entry) => entry.key !== event.key - ); - newInstances.set(event.id, updatedEntries); + if (!current) { + return previous; } - return newInstances; + + next.set( + event.id, + current.filter((entry) => entry.key !== event.key), + ); + return next; }); - } + }, ); - // Request initial snapshots for all instances client.send('get-snapshot', { type: 'get-snapshot', id: 'all', @@ -135,42 +134,67 @@ export default function MMKVPanel() { return; } - // Update entries when selected instance changes - const instanceEntries = instances.get(selectedInstance); - if (instanceEntries) { - setEntries(instanceEntries); - } else { - setLoading(true); - // Request snapshot for the specific instance - client.send('get-snapshot', { - type: 'get-snapshot', - id: selectedInstance, - }); + if (instances.has(selectedInstance)) { + setLoading(false); + return; } + + setLoading(true); + client.send('get-snapshot', { + type: 'get-snapshot', + id: selectedInstance, + }); }, [client, selectedInstance, instances]); - const filteredEntries = entries.filter((entry) => - entry.key.toLowerCase().includes(searchTerm.toLowerCase()) + const entries = selectedInstance + ? (instances.get(selectedInstance) ?? []) + : []; + + const filteredEntries = useMemo( + () => + entries.filter((entry) => + entry.key.toLowerCase().includes(searchTerm.toLowerCase()), + ), + [entries, searchTerm], ); + const updateEntriesForSelectedInstance = ( + mutate: (entries: MMKVEntry[]) => MMKVEntry[], + ) => { + if (!selectedInstance) { + return; + } + + setInstances((previous) => { + const next = new Map(previous); + const current = next.get(selectedInstance); + + if (!current) { + return previous; + } + + next.set(selectedInstance, mutate(current)); + return next; + }); + }; + const handleValueChange = (key: string, newValue: MMKVEntryValue) => { - if (!client || !selectedInstance) return; - - // Determine the entry type based on the new value - let type: 'string' | 'number' | 'boolean' | 'buffer'; - if (typeof newValue === 'string') { - type = 'string'; - } else if (typeof newValue === 'number') { - type = 'number'; - } else if (typeof newValue === 'boolean') { - type = 'boolean'; - } else if (Array.isArray(newValue)) { - type = 'buffer'; - } else { - type = 'string'; // fallback + if (!client || !selectedInstance) { + return; } - const updatedEntry: MMKVEntry = { key, type, value: newValue } as MMKVEntry; + const type = getEntryTypeFromValue(newValue); + + let updatedEntry: MMKVEntry; + if (type === 'string') { + updatedEntry = { key, type: 'string', value: newValue as string }; + } else if (type === 'number') { + updatedEntry = { key, type: 'number', value: newValue as number }; + } else if (type === 'boolean') { + updatedEntry = { key, type: 'boolean', value: newValue as boolean }; + } else { + updatedEntry = { key, type: 'buffer', value: newValue as number[] }; + } client.send('set-entry', { type: 'set-entry', @@ -178,27 +202,15 @@ export default function MMKVPanel() { entry: updatedEntry, }); - // Optimistically update local state - setEntries((prevEntries) => - prevEntries.map((entry) => (entry.key === key ? updatedEntry : entry)) + updateEntriesForSelectedInstance((currentEntries) => + currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry)), ); - - // Update the instances map as well - setInstances((prevInstances) => { - const newInstances = new Map(prevInstances); - const instanceEntries = newInstances.get(selectedInstance); - if (instanceEntries) { - const updatedEntries = instanceEntries.map((entry) => - entry.key === key ? updatedEntry : entry - ); - newInstances.set(selectedInstance, updatedEntries); - } - return newInstances; - }); }; const handleDeleteEntry = (key: string) => { - if (!client || !selectedInstance) return; + if (!client || !selectedInstance) { + return; + } client.send('delete-entry', { type: 'delete-entry', @@ -206,185 +218,135 @@ export default function MMKVPanel() { key, }); - // Optimistically update local state - setEntries((prevEntries) => - prevEntries.filter((entry) => entry.key !== key) + updateEntriesForSelectedInstance((currentEntries) => + currentEntries.filter((entry) => entry.key !== key), ); - - // Update the instances map as well - setInstances((prevInstances) => { - const newInstances = new Map(prevInstances); - const instanceEntries = newInstances.get(selectedInstance); - if (instanceEntries) { - const updatedEntries = instanceEntries.filter( - (entry) => entry.key !== key - ); - newInstances.set(selectedInstance, updatedEntries); - } - return newInstances; - }); }; - const handleAddEntry = (newEntry: MMKVEntry) => { - if (!client || !selectedInstance) return; + const handleAddEntry = (entry: MMKVEntry) => { + if (!client || !selectedInstance) { + return; + } - // Send to device client.send('set-entry', { type: 'set-entry', id: selectedInstance, - entry: newEntry, + entry, }); - // Optimistically update local state - setEntries((prevEntries) => [...prevEntries, newEntry]); - - // Update the instances map as well - setInstances((prevInstances) => { - const newInstances = new Map(prevInstances); - const instanceEntries = newInstances.get(selectedInstance); - if (instanceEntries) { - newInstances.set(selectedInstance, [...instanceEntries, newEntry]); - } - return newInstances; - }); + updateEntriesForSelectedInstance((currentEntries) => [ + ...currentEntries, + entry, + ]); }; + const instanceOptions = [...instances.keys()]; + return ( -
- {/* Toolbar */} -
-
- 💾 - - MMKV Storage - -
+
+
+ + MMKV Storage +
-
- - -
+
- {/* Search and Filter Bar */} -
- +
+ entry.key)} + />
-
- - setSearchTerm(e.target.value)} - className="h-8 w-full pl-8 pr-3 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" - /> -
+ + + + + + +
-
+
{filteredEntries.length} of {entries.length} entries
- {/* Main Content */}
{selectedInstance ? ( - <> - {filteredEntries.length === 0 ? ( -
-
🔍
-

- No entries found -

-

- {searchTerm - ? 'Try adjusting your search terms' - : 'This instance appears to be empty'} -

-
- ) : ( - { - setSelectedEntry(entry); - setShowDetailDialog(true); - }} - loading={loading} - /> - )} - + ) : (
-
🚀
-

+

Welcome to MMKV Inspector

-

- Select an MMKV instance from the dropdown above to start exploring - your data +

+ Select an MMKV instance from the dropdown above to inspect data

)}
- setShowAddDialog(false)} - onAddEntry={handleAddEntry} - existingKeys={entries.map((entry) => entry.key)} - /> - { - setShowDetailDialog(false); - setSelectedEntry(null); - }} + onClose={() => setSelectedEntry(null)} onEdit={(entry) => { - setShowDetailDialog(false); + setSelectedEntry(null); setEditingEntry(entry); - setShowEditDialog(true); }} entry={selectedEntry} /> { - setShowEditDialog(false); - setEditingEntry(null); - }} + onClose={() => setEditingEntry(null)} onEditEntry={(key, newValue) => { handleValueChange(key, newValue); - setShowEditDialog(false); setEditingEntry(null); }} entry={editingEntry} diff --git a/packages/mmkv-plugin/tailwind.config.ts b/packages/mmkv-plugin/tailwind.config.ts deleted file mode 100644 index d756b75a..00000000 --- a/packages/mmkv-plugin/tailwind.config.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Config } from 'tailwindcss'; - -const config: Config = { - darkMode: ['class'], - content: ['./src/ui/**/*.{js,ts,jsx,tsx,mdx}'], - theme: { - extend: { - translate: { - '0.75': '0.1875rem', - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))', - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - keyframes: { - 'accordion-down': { - from: { - height: '0', - }, - to: { - height: 'var(--radix-accordion-content-height)', - }, - }, - 'accordion-up': { - from: { - height: 'var(--radix-accordion-content-height)', - }, - to: { - height: '0', - }, - }, - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - }, - }, - }, - plugins: [require('tailwindcss-animate')], -}; -export default config; diff --git a/packages/mmkv-plugin/tsconfig.json b/packages/mmkv-plugin/tsconfig.json index c95eb1da..349b6a99 100644 --- a/packages/mmkv-plugin/tsconfig.json +++ b/packages/mmkv-plugin/tsconfig.json @@ -11,6 +11,12 @@ "noFallthroughCasesInSwitch": true, "module": "ESNext", "moduleResolution": "bundler", + "baseUrl": ".", + "paths": { + "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"], + "@rozenite/ui": ["../ui/src/index.ts"], + "@rozenite/ui/*": ["../ui/*"] + }, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/packages/storage-plugin/package.json b/packages/storage-plugin/package.json index 9d8262f1..aa4d1b31 100644 --- a/packages/storage-plugin/package.json +++ b/packages/storage-plugin/package.json @@ -22,9 +22,11 @@ }, "dependencies": { "@rozenite/agent-bridge": "workspace:*", + "@rozenite/ui": "workspace:*", "@rozenite/plugin-bridge": "workspace:*" }, "devDependencies": { + "@tailwindcss/postcss": "^4.2.2", "@rozenite/vite-plugin": "workspace:*", "@tanstack/react-table": "^8.21.3", "@types/react": "catalog:", @@ -33,7 +35,6 @@ "postcss": "^8.5.6", "react": "catalog:", "react-dom": "catalog:", - "react-json-tree": "^0.20.0", "react-native": "catalog:", "react-native-mmkv": "^3.3.0", "react-native-mmkv-v3": "npm:react-native-mmkv@^3.0.0", @@ -41,8 +42,9 @@ "react-native-nitro-modules": "*", "react-native-web": "^0.21.2", "rozenite": "workspace:*", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", + "tailwind-variants": "^2.0.0", "typescript": "~5.9.3", "vite": "catalog:" }, diff --git a/packages/storage-plugin/postcss.config.js b/packages/storage-plugin/postcss.config.js index 2aa7205d..a34a3d56 100644 --- a/packages/storage-plugin/postcss.config.js +++ b/packages/storage-plugin/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/packages/storage-plugin/src/ui/add-entry-dialog.tsx b/packages/storage-plugin/src/ui/add-entry-dialog.tsx index d47e3863..74195e2f 100644 --- a/packages/storage-plugin/src/ui/add-entry-dialog.tsx +++ b/packages/storage-plugin/src/ui/add-entry-dialog.tsx @@ -1,5 +1,15 @@ -import { useMemo, useState } from 'react'; -import { X } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { + Button, + Description, + Input, + Label, + ListBox, + Modal, + Select, + TextField, +} from '@rozenite/ui'; +import { Plus } from 'lucide-react'; import type { StorageEntry, StorageEntryType, @@ -14,50 +24,80 @@ const TYPE_OPTIONS: Array<{ value: StorageEntryType; label: string }> = [ { value: 'buffer', label: 'Buffer (Array)' }, ]; -export type AddEntryDialogProps = { +type DialogState = { isOpen: boolean; - onClose: () => void; + title: string; + message: string; + type: 'confirm' | 'alert'; + onConfirm?: () => void; +}; + +const EMPTY_DIALOG_STATE: DialogState = { + isOpen: false, + title: '', + message: '', + type: 'alert', +}; + +export type AddEntryDialogProps = { onAddEntry: (entry: StorageEntry) => void; existingKeys: string[]; supportedTypes: StorageEntryType[]; + isDisabled?: boolean; }; export const AddEntryDialog = ({ - isOpen, - onClose, onAddEntry, existingKeys, supportedTypes, + isDisabled = false, }: AddEntryDialogProps) => { + const [isOpen, setIsOpen] = useState(false); const [newEntryKey, setNewEntryKey] = useState(''); const [newEntryType, setNewEntryType] = useState('string'); const [newEntryValue, setNewEntryValue] = useState(''); - const [confirmDialog, setConfirmDialog] = useState<{ - isOpen: boolean; - title: string; - message: string; - type: 'confirm' | 'alert'; - onConfirm?: () => void; - }>({ isOpen: false, title: '', message: '', type: 'alert' }); + const [confirmDialog, setConfirmDialog] = + useState(EMPTY_DIALOG_STATE); const enabledTypes = useMemo( () => TYPE_OPTIONS.filter((option) => supportedTypes.includes(option.value)), - [supportedTypes] + [supportedTypes], ); - const firstSupportedType = enabledTypes[0]?.value; - const selectedTypeSupported = supportedTypes.includes(newEntryType); + useEffect(() => { + if (isOpen && !selectedTypeSupported && firstSupportedType) { + setNewEntryType(firstSupportedType); + } + }, [firstSupportedType, isOpen, selectedTypeSupported]); + + useEffect(() => { + if (isDisabled && isOpen) { + setNewEntryKey(''); + setNewEntryType(firstSupportedType ?? 'string'); + setNewEntryValue(''); + setConfirmDialog(EMPTY_DIALOG_STATE); + setIsOpen(false); + } + }, [firstSupportedType, isDisabled, isOpen]); + const resetForm = () => { setNewEntryKey(''); setNewEntryType(firstSupportedType ?? 'string'); setNewEntryValue(''); - onClose(); + setConfirmDialog(EMPTY_DIALOG_STATE); + }; + + const closeDialog = () => { + resetForm(); + setIsOpen(false); }; const handleAddEntry = () => { - if (!newEntryKey.trim()) return; + if (!newEntryKey.trim()) { + return; + } if (!selectedTypeSupported) { setConfirmDialog({ @@ -133,178 +173,196 @@ export const AddEntryDialog = ({ } onAddEntry(entry); - - resetForm(); + closeDialog(); }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Escape') { - resetForm(); - return; - } - + const handleInputKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' && newEntryKey.trim() && newEntryValue.trim()) { handleAddEntry(); } }; - if (!isOpen) { - return null; - } - return ( -
-
event.stopPropagation()} - onKeyDown={handleKeyDown} - > -
-

Add New Entry

- -
+ <> + -
-
- - setNewEntryKey(event.target.value)} - placeholder="Enter key name" - 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" - autoFocus - /> -
+ { + setIsOpen(nextOpen); + if (!nextOpen) { + resetForm(); + } + }} + > + + + + + + Add New Entry + + + + + setNewEntryKey(event.target.value)} + onKeyDown={handleInputKeyDown} + placeholder="Enter key name" + value={newEntryKey} + variant="secondary" + /> + -
- - { + setNewEntryType( + (key == null ? '' : String(key)) as StorageEntryType, + ); + setNewEntryValue(''); + }} + value={newEntryType || null} + variant="secondary" > - {option.label} - {!isSupported ? ' (Not supported)' : ''} - - ); - })} - - {!selectedTypeSupported && ( -

- Selected type is not supported by this storage. -

- )} -
+ + + + + + + {TYPE_OPTIONS.map((option) => { + const isSupported = supportedTypes.includes( + option.value, + ); -
- - {newEntryType === 'boolean' ? ( - - ) : ( - setNewEntryValue(event.target.value)} - placeholder={ - newEntryType === 'string' - ? 'Enter string value' - : newEntryType === 'number' - ? 'Enter number value' - : newEntryType === 'buffer' - ? 'Enter array as JSON, e.g., [1, 2, 3]' - : 'Enter value' - } - 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" - /> - )} - {newEntryType === 'buffer' && ( -

- Enter as JSON array of numbers, e.g., [1, 2, 3, 255] -

- )} -
-
+ return ( + + {isSupported + ? option.label + : `${option.label} (Not supported)`} + + ); + })} + + + + {!selectedTypeSupported ? ( + + Selected type is not supported by this storage. + + ) : null} +
-
- - -
-
+ {newEntryType === 'boolean' ? ( +
+ + +
+ ) : ( + + + setNewEntryValue(event.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={ + newEntryType === 'string' + ? 'Enter string value' + : newEntryType === 'number' + ? 'Enter number value' + : newEntryType === 'buffer' + ? 'Enter array as JSON, e.g., [1, 2, 3]' + : 'Enter value' + } + value={newEntryValue} + variant="secondary" + /> + {newEntryType === 'buffer' ? ( + + Enter as JSON array of numbers, e.g., [1, 2, 3, 255] + + ) : null} + + )} + + + + + + + + + setConfirmDialog((previous) => ({ ...previous, isOpen: false }))} + message={confirmDialog.message} + onClose={() => + setConfirmDialog((previous) => ({ ...previous, isOpen: false })) + } onConfirm={() => { if (confirmDialog.onConfirm) { confirmDialog.onConfirm(); } }} title={confirmDialog.title} - message={confirmDialog.message} type={confirmDialog.type} /> -
+ ); }; diff --git a/packages/storage-plugin/src/ui/confirm-dialog.tsx b/packages/storage-plugin/src/ui/confirm-dialog.tsx index fe024a34..4c7a25dc 100644 --- a/packages/storage-plugin/src/ui/confirm-dialog.tsx +++ b/packages/storage-plugin/src/ui/confirm-dialog.tsx @@ -1,4 +1,5 @@ -import { X, AlertTriangle, Info } from 'lucide-react'; +import { AlertDialog, Button } from '@rozenite/ui'; +import { AlertTriangle, Info } from 'lucide-react'; export type ConfirmDialogProps = { isOpen: boolean; @@ -21,80 +22,60 @@ export const ConfirmDialog = ({ confirmText = 'Confirm', cancelText = 'Cancel', }: ConfirmDialogProps) => { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } else if (e.key === 'Enter') { - onConfirm(); - } - }; - - const handleConfirm = () => { - onConfirm(); - onClose(); - }; - - const handleCancel = () => { - onClose(); - }; - - if (!isOpen) return null; + const isConfirm = type === 'confirm'; return ( -
{ + if (!nextOpen) { + onClose(); + } + }} > -
e.stopPropagation()} - onKeyDown={handleKeyDown} + -
-
- {type === 'confirm' ? ( - - ) : ( - - )} -

{title}

-
- -
- -
-

{message}

-
- - {/* Dialog Actions */} -
- {type === 'confirm' && ( - - )} - -
-
-
+ + + {isConfirm ? ( + + ) : ( + + )} + + + {title} + + + + {message} + + + {isConfirm ? ( + + ) : null} + + + + + + ); }; diff --git a/packages/storage-plugin/src/ui/edit-entry-dialog.tsx b/packages/storage-plugin/src/ui/edit-entry-dialog.tsx index a0e98e51..860458dd 100644 --- a/packages/storage-plugin/src/ui/edit-entry-dialog.tsx +++ b/packages/storage-plugin/src/ui/edit-entry-dialog.tsx @@ -1,5 +1,17 @@ import { useEffect, useMemo, useState } from 'react'; -import { X, Edit3 } from 'lucide-react'; +import { + Button, + Chip, + Description, + Input, + Label, + ListBox, + Modal, + Select, + Surface, + TextField, +} from '@rozenite/ui'; +import { Edit3 } from 'lucide-react'; import type { StorageEntry, StorageEntryType, @@ -8,47 +20,90 @@ import type { import { ConfirmDialog } from './confirm-dialog'; export type EditEntryDialogProps = { - isOpen: boolean; onClose: () => void; onEditEntry: (key: string, newValue: StorageEntryValue) => void; entry: StorageEntry | null; supportedTypes: StorageEntryType[]; }; +type DialogState = { + isOpen: boolean; + title: string; + message: string; + type: 'confirm' | 'alert'; + onConfirm?: () => void; +}; + +const EMPTY_DIALOG_STATE: DialogState = { + isOpen: false, + title: '', + message: '', + type: 'alert', +}; + +const typeColorMap: Record< + StorageEntryType, + 'success' | 'warning' | 'accent' | 'default' +> = { + string: 'success', + number: 'default', + boolean: 'warning', + buffer: 'accent', +}; + +const getInputType = (type: StorageEntryType) => + type === 'number' ? 'number' : 'text'; + +const getPlaceholder = (type: StorageEntryType) => { + if (type === 'string') { + return 'Enter string value'; + } + + if (type === 'number') { + return 'Enter number value'; + } + + if (type === 'boolean') { + return 'Enter true or false'; + } + + return 'Enter array as JSON, e.g., [1, 2, 3]'; +}; + export const EditEntryDialog = ({ - isOpen, onClose, onEditEntry, entry, supportedTypes, }: EditEntryDialogProps) => { const [editValue, setEditValue] = useState(''); - const [confirmDialog, setConfirmDialog] = useState<{ - isOpen: boolean; - title: string; - message: string; - type: 'confirm' | 'alert'; - onConfirm?: () => void; - }>({ isOpen: false, title: '', message: '', type: 'alert' }); + const [confirmDialog, setConfirmDialog] = + useState(EMPTY_DIALOG_STATE); useEffect(() => { - if (entry && isOpen) { - setEditValue(Array.isArray(entry.value) ? JSON.stringify(entry.value) : String(entry.value)); + if (entry) { + setEditValue( + Array.isArray(entry.value) ? JSON.stringify(entry.value) : String(entry.value), + ); + setConfirmDialog(EMPTY_DIALOG_STATE); } - }, [entry, isOpen]); + }, [entry]); const isTypeSupported = useMemo( () => !!entry && supportedTypes.includes(entry.type), - [entry, supportedTypes] + [entry, supportedTypes], ); const resetForm = () => { setEditValue(''); + setConfirmDialog(EMPTY_DIALOG_STATE); onClose(); }; const handleEditEntry = () => { - if (!entry) return; + if (!entry) { + return; + } if (!isTypeSupported) { setConfirmDialog({ @@ -81,7 +136,10 @@ export const EditEntryDialog = ({ break; case 'buffer': newValue = JSON.parse(editValue); - if (!Array.isArray(newValue) || !newValue.every((v) => typeof v === 'number')) { + if ( + !Array.isArray(newValue) || + !newValue.every((value) => typeof value === 'number') + ) { throw new Error('Buffer must be an array of numbers'); } break; @@ -104,177 +162,161 @@ export const EditEntryDialog = ({ resetForm(); }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Escape') { - resetForm(); - } else if (event.key === 'Enter' && editValue.trim()) { + const handleInputKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && editValue.trim()) { handleEditEntry(); } }; - const getInputType = (type: StorageEntryType) => { - if (type === 'number') { - return 'number'; - } - - return 'text'; - }; - - const getPlaceholder = (type: StorageEntryType) => { - if (type === 'string') { - return 'Enter string value'; - } - - if (type === 'number') { - return 'Enter number value'; - } - - if (type === 'boolean') { - return 'Enter true or false'; - } - - return 'Enter array as JSON, e.g., [1, 2, 3]'; - }; - - const getTypeColorClass = (type: StorageEntryType) => { - if (type === 'string') { - return 'bg-green-600'; - } - - if (type === 'number') { - return 'bg-blue-600'; - } - - if (type === 'boolean') { - return 'bg-yellow-600'; - } - - return 'bg-purple-600'; - }; - - if (!isOpen || !entry) { - return null; - } - return ( -
-
event.stopPropagation()} - onKeyDown={handleKeyDown} + <> + { + if (!nextOpen) { + resetForm(); + } + }} > -
-
- -

Edit Entry

-
- -
- -
-
- -
- {entry.key} -
-

Key cannot be changed during editing

-
+ + + + + + + + + Edit Entry + + + {entry ? ( + <> +
+ + + {entry.key} + + + Key cannot be changed during editing. + +
-
- -
- - {entry.type} - -
- {!isTypeSupported ? ( -

- This storage does not support {entry.type} values. -

- ) : ( -

Type cannot be changed during editing

- )} -
+
+ + + {entry.type} + + + {isTypeSupported + ? 'Type cannot be changed during editing.' + : `This storage does not support ${entry.type} values.`} + +
-
- - {entry.type === 'boolean' ? ( - - ) : ( - setEditValue(event.target.value)} - placeholder={getPlaceholder(entry.type)} - 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" - autoFocus - /> - )} - {entry.type === 'buffer' && ( -

- Enter as JSON array of numbers, e.g., [1, 2, 3, 255] -

- )} -
-
- -
- - -
-
+ {entry.type === 'boolean' ? ( +
+ + +
+ ) : ( + + + setEditValue(event.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={getPlaceholder(entry.type)} + value={editValue} + variant="secondary" + /> + {entry.type === 'buffer' ? ( + + Enter as JSON array of numbers, e.g., [1, 2, 3, 255] + + ) : null} + + )} + + ) : null} + + + + + + + + + setConfirmDialog((previous) => ({ ...previous, isOpen: false }))} + message={confirmDialog.message} + onClose={() => + setConfirmDialog((previous) => ({ ...previous, isOpen: false })) + } onConfirm={() => { if (confirmDialog.onConfirm) { confirmDialog.onConfirm(); } }} title={confirmDialog.title} - message={confirmDialog.message} type={confirmDialog.type} /> -
+ ); }; diff --git a/packages/storage-plugin/src/ui/editable-table.tsx b/packages/storage-plugin/src/ui/editable-table.tsx index 8f1f0d50..721ee7c5 100644 --- a/packages/storage-plugin/src/ui/editable-table.tsx +++ b/packages/storage-plugin/src/ui/editable-table.tsx @@ -1,15 +1,15 @@ import { useMemo, useState } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; import { createColumnHelper, flexRender, getCoreRowModel, - getFilteredRowModel, getSortedRowModel, - type ColumnDef, type SortingState, useReactTable, } from '@tanstack/react-table'; -import { Edit3, Loader2, Trash2 } from 'lucide-react'; +import { Button, Chip, Table } from '@rozenite/ui'; +import { ChevronUp, Edit3, Inbox, Loader2, Trash2 } from 'lucide-react'; import type { StorageEntry, StorageEntryType, @@ -25,9 +25,68 @@ export type EditableTableProps = { onDeleteEntry?: (key: string) => void; onRowClick?: (entry: StorageEntry) => void; loading?: boolean; + searchTerm?: string; +}; + +const typeColorMap: Record< + string, + 'success' | 'danger' | 'warning' | 'accent' | 'default' +> = { + string: 'success', + number: 'default', + boolean: 'warning', + buffer: 'accent', }; const columnHelper = createColumnHelper(); +type TableSortDescriptor = NonNullable< + ComponentProps['sortDescriptor'] +>; + +function toSortDescriptor( + sorting: SortingState, +): TableSortDescriptor | undefined { + const firstSort = sorting[0]; + + if (!firstSort) { + return undefined; + } + + return { + column: firstSort.id, + direction: firstSort.desc ? 'descending' : 'ascending', + }; +} + +function toSortingState(descriptor: TableSortDescriptor): SortingState { + return [ + { + desc: descriptor.direction === 'descending', + id: String(descriptor.column), + }, + ]; +} + +function SortableColumnHeader({ + children, + sortDirection, +}: { + children: ReactNode; + sortDirection?: 'ascending' | 'descending'; +}) { + return ( + + {children} + {sortDirection ? ( + + ) : null} + + ); +} export const EditableTable = ({ data, @@ -36,81 +95,81 @@ export const EditableTable = ({ onDeleteEntry, onRowClick, loading = false, + searchTerm = '', }: EditableTableProps) => { const [editingEntry, setEditingEntry] = useState(null); - const [showEditDialog, setShowEditDialog] = useState(false); const [sorting, setSorting] = useState([]); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; entryKey: string; }>({ isOpen: false, entryKey: '' }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const columns = useMemo[]>( + const columns = useMemo( () => [ columnHelper.accessor('key', { header: 'Key', enableSorting: true, cell: ({ getValue }) => ( -
{getValue()}
+ + {getValue()} + ), }), columnHelper.accessor('type', { header: 'Type', enableSorting: true, - cell: ({ getValue }) => { - const type = getValue() as StorageEntryType; + cell: (info) => { + const type = info.getValue() as StorageEntryType; + return ( -
- - {type} - -
+ + {type} + ); }, }), columnHelper.accessor('value', { header: 'Value', - cell: ({ row }) => { - const entry = row.original; - return ( -
-
{formatValue(entry)}
- -
- ); - }, + cell: ({ row }) => ( +
{formatValue(row.original)}
+ ), }), columnHelper.display({ id: 'actions', header: 'Actions', cell: ({ row }) => ( -
- + + +
), }), ], - [onDeleteEntry] + [onDeleteEntry, onValueChange], ); const table = useReactTable({ @@ -119,13 +178,12 @@ export const EditableTable = ({ state: { sorting }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), }); + const sortDescriptor = useMemo(() => toSortDescriptor(sorting), [sorting]); const handleEdit = (entry: StorageEntry) => { setEditingEntry(entry); - setShowEditDialog(true); }; const handleEditEntry = (key: string, newValue: StorageEntryValue) => { @@ -134,12 +192,10 @@ export const EditableTable = ({ } setEditingEntry(null); - setShowEditDialog(false); }; const handleCloseEditDialog = () => { setEditingEntry(null); - setShowEditDialog(false); }; const handleDelete = (key: string) => { @@ -158,85 +214,108 @@ export const EditableTable = ({ if (loading) { return ( -
- -

Loading entries...

+
+ +

Loading entries...

); } return ( <> - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + + ) : ( + flexRender( + header.column.columnDef.header, + header.getContext(), + ) + ) + } + ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - { - const target = event.target as HTMLElement; - if ( - target.tagName === 'BUTTON' || - target.closest('button') || - target.tagName === 'INPUT' || - target.closest('input') - ) { - return; - } - - if (onRowClick) { - onRowClick(row.original); - } - }} + + ( +
+ + + {searchTerm + ? 'No results found' + : 'This storage appears to be empty'} + +
+ )} > - {row.getVisibleCells().map((cell) => ( -
+ {table.getRowModel().rows.map((row) => ( + { + const target = event.target as HTMLElement; + if ( + target.tagName === 'BUTTON' || + target.closest('button') || + target.tagName === 'INPUT' || + target.closest('input') + ) { + return; + } + + onRowClick?.(row.original); + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + ))} - - ))} - -
+ + + setSorting(toSortingState(descriptor)) + } + sortDescriptor={sortDescriptor} + > + + {table.getHeaderGroups()[0]?.headers.map((header) => ( + -
- {header.isPlaceholder - ? null - : flexRender( + {({ sortDirection }) => + header.column.getCanSort() ? ( + + {flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} - {header.column.getCanSort() && ( - - {{ - asc: '↑', - desc: '↓', - }[header.column.getIsSorted() as string] ?? '↕'} - - )} -
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
+ + + + { - if (type === 'string') { - return 'bg-green-600'; - } - - if (type === 'number') { - return 'bg-blue-600'; - } - - if (type === 'boolean') { - return 'bg-yellow-600'; - } - - return 'bg-purple-600'; -}; - const formatValue = (entry: StorageEntry) => { if (entry.type === 'string') { - return "{entry.value}"; + return ( + "{entry.value}" + ); } if (entry.type === 'number') { - return {entry.value}; + return ( + {entry.value} + ); } if (entry.type === 'boolean') { return ( - + {entry.value ? 'true' : 'false'} ); @@ -294,5 +361,5 @@ const formatValue = (entry: StorageEntry) => { ? `[${entry.value.slice(0, 5).join(', ')}, ...${entry.value.length - 5} more]` : `[${entry.value.join(', ')}]`; - return {displayValue}; + return {displayValue}; }; diff --git a/packages/storage-plugin/src/ui/entry-detail-dialog.tsx b/packages/storage-plugin/src/ui/entry-detail-dialog.tsx index 87a32af3..78e690c7 100644 --- a/packages/storage-plugin/src/ui/entry-detail-dialog.tsx +++ b/packages/storage-plugin/src/ui/entry-detail-dialog.tsx @@ -1,220 +1,173 @@ -import { X, Info, Edit3 } from 'lucide-react'; -import { JSONTree } from 'react-json-tree'; -import { StorageEntry } from '../shared/types'; import { useMemo } from 'react'; +import { + Button, + Chip, + Description, + JsonInspector, + Label, + Modal, + parseJsonForInspection, + Surface, +} from '@rozenite/ui'; +import { Edit3, Info } from 'lucide-react'; +import type { StorageEntry, StorageEntryType } from '../shared/types'; export type EntryDetailDialogProps = { - isOpen: boolean; onClose: () => void; onEdit?: (entry: StorageEntry) => void; entry: StorageEntry | null; }; -const jsonTreeTheme = { - base00: 'transparent', - base01: '#374151', // bg-gray-700 - base02: '#4b5563', // bg-gray-600 - base03: '#6b7280', // text-gray-500 - base04: '#9ca3af', // text-gray-400 - base05: '#d1d5db', // text-gray-300 - base06: '#e5e7eb', // text-gray-200 - base07: '#f9fafb', // text-gray-100 - base08: '#ef4444', // text-red-500 - base09: '#f59e0b', // text-yellow-500 - base0A: '#10b981', // text-green-500 - base0B: '#3b82f6', // text-blue-500 - base0C: '#06b6d4', // text-cyan-500 - base0D: '#8b5cf6', // text-purple-500 - base0E: '#ec4899', // text-pink-500 - base0F: '#f97316', // text-orange-500 +const typeColorMap: Record< + StorageEntryType, + 'success' | 'warning' | 'accent' | 'default' +> = { + string: 'success', + number: 'default', + boolean: 'warning', + buffer: 'accent', }; -const jsonSafeParse = (value: string): Record | unknown[] | null => { - try { - const parsed = JSON.parse(value) as unknown; - - if (Array.isArray(parsed)) { - return parsed; - } - - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - - return null; - } catch { - return null; +const formatValue = (entry: StorageEntry) => { + if (entry.type === 'string') { + return ( + + "{entry.value}" + + ); } -}; -const getTypeColorClass = (type: string) => { - switch (type) { - case 'string': - return 'bg-green-600'; - case 'number': - return 'bg-blue-600'; - case 'boolean': - return 'bg-yellow-600'; - case 'buffer': - return 'bg-purple-600'; - default: - return 'bg-gray-600'; + if (entry.type === 'number') { + return ( + {entry.value} + ); } -}; -const formatValue = (entry: StorageEntry) => { - switch (entry.type) { - case 'string': - return ( - - "{entry.value as string}" - - ); - case 'number': - return ( - {entry.value as number} - ); - case 'boolean': - return ( - - {entry.value ? 'true' : 'false'} - - ); - case 'buffer': { - const bufferArray = entry.value as number[]; - return ( - - [{bufferArray.join(', ')}] - - ); - } - default: - return Unknown; + if (entry.type === 'boolean') { + return ( + + {entry.value ? 'true' : 'false'} + + ); } + + return ( + + [{entry.value.join(', ')}] + + ); }; export const EntryDetailDialog = ({ - isOpen, onClose, onEdit, entry, }: EntryDetailDialogProps) => { const isStringValue = entry?.type === 'string'; - const stringValue = isStringValue ? (entry.value as string) : ''; - const jsonValue = useMemo( - () => (isStringValue ? jsonSafeParse(stringValue) : null), - [isStringValue, stringValue] + const stringValue = isStringValue ? entry.value : ''; + const jsonParseResult = useMemo( + () => + isStringValue + ? parseJsonForInspection(stringValue, 'object-or-array') + : { ok: false as const, value: null }, + [isStringValue, stringValue], ); - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - - if (!isOpen || !entry) return null; - return ( -
{ + if (!nextOpen) { + onClose(); + } + }} > -
e.stopPropagation()} - onKeyDown={handleKeyDown} - > -
-
- -

- Entry Details -

-
- -
- -
- {/* Key Display */} -
- -
- {entry.key} -
-
- - {/* Type Display */} -
- -
- - {entry.type} - -
-
- - {/* Value Display */} -
- -
- {jsonValue ? ( - keyPath.length <= 2} - /> - ) : ( -
{formatValue(entry)}
- )} -
-
-
- - {/* Dialog Actions */} -
- {onEdit && ( - - )} - -
-
-
+ + + + + + Entry Details + + + {entry ? ( + <> +
+ + + {entry.key} + +
+ +
+ + + {entry.type} + +
+ +
+ + + {jsonParseResult.ok ? ( + + keyPath.length <= 2 + } + theme="dark" + /> + ) : ( +
+ {formatValue(entry)} +
+ )} +
+ {entry.type === 'string' && jsonParseResult.ok ? ( + + Displaying parsed JSON from the stored string value. + + ) : null} +
+ + ) : null} +
+ + {entry && onEdit ? ( + + ) : null} + + + + + + ); }; diff --git a/packages/storage-plugin/src/ui/globals.css b/packages/storage-plugin/src/ui/globals.css index 2bf47da0..77390a0f 100644 --- a/packages/storage-plugin/src/ui/globals.css +++ b/packages/storage-plugin/src/ui/globals.css @@ -1,6 +1,5 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@import '@rozenite/ui/styles.css'; @layer utilities { .text-balance { @@ -12,87 +11,6 @@ } } -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - /* Custom scrollbar styles to match plugin theme */ ::-webkit-scrollbar { width: 8px; diff --git a/packages/storage-plugin/src/ui/panel.tsx b/packages/storage-plugin/src/ui/panel.tsx index 9425006b..970155d1 100644 --- a/packages/storage-plugin/src/ui/panel.tsx +++ b/packages/storage-plugin/src/ui/panel.tsx @@ -1,6 +1,6 @@ import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; import { useEffect, useMemo, useState } from 'react'; -import { Search, Plus } from 'lucide-react'; +import { ListBox, Select, SearchField } from '@rozenite/ui'; import type { StorageDeleteEntryEvent, StorageEventMap, @@ -28,7 +28,9 @@ type StorageSnapshotState = { entries: StorageEntry[]; }; -const getEntryTypeFromValue = (value: StorageEntryValue): StorageEntry['type'] => { +const getEntryTypeFromValue = ( + value: StorageEntryValue, +): StorageEntry['type'] => { if (typeof value === 'string') { return 'string'; } @@ -46,18 +48,15 @@ const getEntryTypeFromValue = (value: StorageEntryValue): StorageEntry['type'] = export default function StoragePanel() { const [snapshots, setSnapshots] = useState>( - new Map() - ); - const [selectedStorageViewId, setSelectedStorageViewId] = useState( - null + new Map(), ); + const [selectedStorageViewId, setSelectedStorageViewId] = useState< + string | null + >(null); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(''); - const [showAddDialog, setShowAddDialog] = useState(false); const [selectedEntry, setSelectedEntry] = useState(null); - const [showDetailDialog, setShowDetailDialog] = useState(false); const [editingEntry, setEditingEntry] = useState(null); - const [showEditDialog, setShowEditDialog] = useState(false); const client = useRozeniteDevToolsClient({ pluginId: '@rozenite/storage-plugin', @@ -71,61 +70,61 @@ export default function StoragePanel() { const snapshotSubscription = client.onMessage( 'snapshot', (event: StorageSnapshotEvent) => { - const viewId = getStorageViewId(event.target); - setSnapshots((previous) => { - const next = new Map(previous); - next.set(viewId, { - target: event.target, - adapterName: event.adapterName, - storageName: event.storageName, - capabilities: event.capabilities, - entries: event.entries, - }); + const viewId = getStorageViewId(event.target); + setSnapshots((previous) => { + const next = new Map(previous); + next.set(viewId, { + target: event.target, + adapterName: event.adapterName, + storageName: event.storageName, + capabilities: event.capabilities, + entries: event.entries, + }); - if (previous.size === 0 && !selectedStorageViewId) { - setSelectedStorageViewId(viewId); - } + if (previous.size === 0 && !selectedStorageViewId) { + setSelectedStorageViewId(viewId); + } - return next; - }); + return next; + }); - if (viewId === selectedStorageViewId) { - setLoading(false); - } - } + if (viewId === selectedStorageViewId) { + setLoading(false); + } + }, ); const setEntrySubscription = client.onMessage( 'set-entry', (event: StorageSetEntryEvent) => { - const viewId = getStorageViewId(event.target); - setSnapshots((previous) => { - const next = new Map(previous); - const current = next.get(viewId); + const viewId = getStorageViewId(event.target); + setSnapshots((previous) => { + const next = new Map(previous); + const current = next.get(viewId); - if (!current) { - return previous; - } + if (!current) { + return previous; + } - const existingIndex = current.entries.findIndex( - (entry) => entry.key === event.entry.key - ); + const existingIndex = current.entries.findIndex( + (entry) => entry.key === event.entry.key, + ); - const entries = - existingIndex >= 0 - ? current.entries.map((entry) => - entry.key === event.entry.key ? event.entry : entry - ) - : [...current.entries, event.entry]; + const entries = + existingIndex >= 0 + ? current.entries.map((entry) => + entry.key === event.entry.key ? event.entry : entry, + ) + : [...current.entries, event.entry]; - next.set(viewId, { - ...current, - entries, - }); + next.set(viewId, { + ...current, + entries, + }); - return next; - }); - } + return next; + }); + }, ); const deleteEntrySubscription = client.onMessage( @@ -148,7 +147,7 @@ export default function StoragePanel() { return next; }); - } + }, ); client.send('get-snapshot', { @@ -178,7 +177,7 @@ export default function StoragePanel() { const separatorIndex = selectedStorageViewId.indexOf(':'); if (separatorIndex < 0) { console.warn( - `[Rozenite] Storage Plugin: Invalid storage view id "${selectedStorageViewId}".` + `[Rozenite] Storage Plugin: Invalid storage view id "${selectedStorageViewId}".`, ); setLoading(false); return; @@ -198,7 +197,7 @@ export default function StoragePanel() { }, [client, selectedStorageViewId, snapshots]); const selectedStorage = selectedStorageViewId - ? snapshots.get(selectedStorageViewId) ?? null + ? (snapshots.get(selectedStorageViewId) ?? null) : null; const entries = selectedStorage?.entries ?? []; @@ -206,15 +205,15 @@ export default function StoragePanel() { const filteredEntries = useMemo( () => entries.filter((entry) => - entry.key.toLowerCase().includes(searchTerm.toLowerCase()) + entry.key.toLowerCase().includes(searchTerm.toLowerCase()), ), - [entries, searchTerm] + [entries, searchTerm], ); const supportedTypes = selectedStorage?.capabilities.supportedTypes ?? []; const updateEntriesForSelectedStorage = ( - mutate: (entries: StorageEntry[]) => StorageEntry[] + mutate: (entries: StorageEntry[]) => StorageEntry[], ) => { if (!selectedStorageViewId) { return; @@ -266,7 +265,7 @@ export default function StoragePanel() { }); updateEntriesForSelectedStorage((currentEntries) => - currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry)) + currentEntries.map((entry) => (entry.key === key ? updatedEntry : entry)), ); }; @@ -282,7 +281,7 @@ export default function StoragePanel() { }); updateEntriesForSelectedStorage((currentEntries) => - currentEntries.filter((entry) => entry.key !== key) + currentEntries.filter((entry) => entry.key !== key), ); }; @@ -297,7 +296,10 @@ export default function StoragePanel() { entry, }); - updateEntriesForSelectedStorage((currentEntries) => [...currentEntries, entry]); + updateEntriesForSelectedStorage((currentEntries) => [ + ...currentEntries, + entry, + ]); }; const storageOptions = [...snapshots.entries()].map(([viewId, snapshot]) => ({ @@ -306,132 +308,114 @@ export default function StoragePanel() { })); return ( -
-
+
+
- Storage + + Storage +
-
- - + setSelectedStorageViewId( + typeof value === 'string' + ? value + : value == null + ? null + : String(value), + ) + } + isDisabled={snapshots.size === 0} + > + + + + + + + {storageOptions.map((option) => ( + {option.label} - - )) - )} - -
+ + + ))} + + +
-
- +
+ entry.key)} + supportedTypes={supportedTypes} + />
-
- - setSearchTerm(event.target.value)} - className="h-8 w-full pl-8 pr-3 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" - /> -
+ + + + + + +
-
+
{filteredEntries.length} of {entries.length} entries
{selectedStorage ? ( - filteredEntries.length === 0 ? ( -
-

- No entries found -

-

- {searchTerm - ? 'Try adjusting your search terms' - : 'This storage appears to be empty'} -

-
- ) : ( - { - setSelectedEntry(entry); - setShowDetailDialog(true); - }} - loading={loading} - /> - ) + ) : (
-

+

Welcome to Storage Inspector

-

+

Select a storage from the dropdown above to inspect data

)}
- setShowAddDialog(false)} - onAddEntry={handleAddEntry} - existingKeys={entries.map((entry) => entry.key)} - supportedTypes={supportedTypes} - /> - { - setShowDetailDialog(false); - setSelectedEntry(null); - }} + onClose={() => setSelectedEntry(null)} onEdit={(entry) => { - setShowDetailDialog(false); + setSelectedEntry(null); setEditingEntry(entry); - setShowEditDialog(true); }} entry={selectedEntry} /> { - setShowEditDialog(false); - setEditingEntry(null); - }} + onClose={() => setEditingEntry(null)} onEditEntry={(key, newValue) => { handleValueChange(key, newValue); - setShowEditDialog(false); setEditingEntry(null); }} supportedTypes={supportedTypes} diff --git a/packages/storage-plugin/tailwind.config.ts b/packages/storage-plugin/tailwind.config.ts deleted file mode 100644 index d756b75a..00000000 --- a/packages/storage-plugin/tailwind.config.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Config } from 'tailwindcss'; - -const config: Config = { - darkMode: ['class'], - content: ['./src/ui/**/*.{js,ts,jsx,tsx,mdx}'], - theme: { - extend: { - translate: { - '0.75': '0.1875rem', - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))', - }, - sidebar: { - DEFAULT: 'hsl(var(--sidebar-background))', - foreground: 'hsl(var(--sidebar-foreground))', - primary: 'hsl(var(--sidebar-primary))', - 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', - accent: 'hsl(var(--sidebar-accent))', - 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', - border: 'hsl(var(--sidebar-border))', - ring: 'hsl(var(--sidebar-ring))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - keyframes: { - 'accordion-down': { - from: { - height: '0', - }, - to: { - height: 'var(--radix-accordion-content-height)', - }, - }, - 'accordion-up': { - from: { - height: 'var(--radix-accordion-content-height)', - }, - to: { - height: '0', - }, - }, - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - }, - }, - }, - plugins: [require('tailwindcss-animate')], -}; -export default config; diff --git a/packages/storage-plugin/tsconfig.json b/packages/storage-plugin/tsconfig.json index c1d4d562..f4002786 100644 --- a/packages/storage-plugin/tsconfig.json +++ b/packages/storage-plugin/tsconfig.json @@ -13,7 +13,9 @@ "moduleResolution": "bundler", "baseUrl": ".", "paths": { - "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"] + "@rozenite/plugin-bridge": ["../plugin-bridge/src/index.ts"], + "@rozenite/ui": ["../ui/src/index.ts"], + "@rozenite/ui/*": ["../ui/*"] }, "resolveJsonModule": true, "isolatedModules": true, diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 00000000..0c1d296b --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,76 @@ +{ + "name": "@rozenite/ui", + "version": "1.7.0", + "description": "Shared UI primitives and plugin layout components for Rozenite web plugins.", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "src", + "styles.css" + ], + "keywords": [ + "react", + "devtools", + "ui", + "rozenite", + "plugins" + ], + "homepage": "https://github.com/callstackincubator/rozenite#readme", + "bugs": { + "url": "https://github.com/callstackincubator/rozenite/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/callstackincubator/rozenite.git" + }, + "author": "Szymon Chmal ", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "vite build", + "typecheck": "tsc -p tsconfig.lib.json --noEmit", + "lint": "eslint .", + "clean": "rm -rf dist" + }, + "exports": { + "./package.json": "./package.json", + "./styles.css": "./styles.css", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "dependencies": { + "@heroui/react": "^3.0.1", + "@heroui/styles": "^3.0.1", + "@tanstack/react-table": "^8.21.3", + "lucide-react": "^0.263.1", + "react-json-tree": "^0.20.0", + "react-virtuoso": "^4.14.1", + "tailwindcss": "^4.2.2", + "tailwind-variants": "^2.0.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "typescript": "~5.9.3", + "vite": "catalog:" + }, + "peerDependencies": { + "react": ">=19", + "react-dom": ">=19" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/ui/src/hooks/useCopyToClipboard.ts b/packages/ui/src/hooks/useCopyToClipboard.ts new file mode 100644 index 00000000..1a6c9e9b --- /dev/null +++ b/packages/ui/src/hooks/useCopyToClipboard.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +const copyToClipboard = async (value: string) => { + if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(value); + return; + } + + if (typeof document === 'undefined') { + throw new Error('Clipboard is not available in this environment.'); + } + + const textArea = document.createElement('textarea'); + textArea.value = value; + textArea.setAttribute('readonly', 'true'); + textArea.style.position = 'absolute'; + textArea.style.left = '-9999px'; + + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); +}; + +export const useCopyToClipboard = (resetDelayMs = 1000) => { + const [isCopied, setIsCopied] = useState(false); + const timeoutRef = useRef(null); + + useEffect(() => { + return () => { + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + }; + }, []); + + const copy = useCallback( + async (value: string) => { + await copyToClipboard(value); + setIsCopied(true); + + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + + timeoutRef.current = window.setTimeout(() => { + setIsCopied(false); + }, resetDelayMs); + }, + [resetDelayMs] + ); + + return { isCopied, copy }; +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 00000000..4aaa5bf1 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,37 @@ +export { + Accordion, + AlertDialog, + Button, + Card, + Chip, + Description, + Drawer, + FieldError, + Header, + Input, + Label, + ListBox, + Modal, + Select, + Separator, + SearchField, + Surface, + Switch, + Table, + Tabs, + TextArea, + TextArea as Textarea, + TextField, + Tooltip, + useOverlayState, +} from '@heroui/react'; + +export { createColumnHelper } from '@tanstack/react-table'; +export type { ColumnDef, Row, SortingState } from '@tanstack/react-table'; +export { JsonInspector } from './json-inspector'; +export type { JsonInspectorProps, JsonInspectorTheme } from './json-inspector'; +export { parseJsonForInspection } from './utils/json'; +export type { + JsonInspectionParseMode, + JsonInspectionParseResult, +} from './utils/json'; diff --git a/packages/ui/src/json-inspector.tsx b/packages/ui/src/json-inspector.tsx new file mode 100644 index 00000000..62b188c2 --- /dev/null +++ b/packages/ui/src/json-inspector.tsx @@ -0,0 +1,99 @@ +import { useMemo } from 'react'; +import { Button } from '@heroui/react'; +import { Check, Copy } from 'lucide-react'; +import { JSONTree, type ShouldExpandNodeInitially } from 'react-json-tree'; +import { useCopyToClipboard } from './hooks/useCopyToClipboard'; +import { cn } from './utils/cn'; + +export type JsonInspectorTheme = 'dark' | 'light'; + +export type JsonInspectorProps = { + className?: string; + collectionLimit?: number; + copyable?: boolean; + data: unknown; + hideRoot?: boolean; + shouldExpandNodeInitially?: ShouldExpandNodeInitially; + sortObjectKeys?: boolean | ((a: unknown, b: unknown) => number); + theme?: JsonInspectorTheme; +}; + +const jsonTreeDarkTheme = { + base00: 'transparent', + base01: 'var(--surface-secondary)', + base02: 'var(--surface-tertiary)', + base03: 'var(--muted)', + base04: 'var(--muted)', + base05: 'var(--foreground)', + base06: 'var(--foreground)', + base07: 'var(--foreground)', + base08: 'var(--danger)', + base09: 'var(--warning)', + base0A: 'var(--success)', + base0B: 'var(--accent)', + base0C: 'var(--info)', + base0D: 'var(--accent)', + base0E: 'var(--warning)', + base0F: 'var(--danger)', +}; + +const jsonTreeLightTheme = { + ...jsonTreeDarkTheme, + base00: 'transparent', +}; + +const stringifyForClipboard = (value: unknown) => { + try { + return JSON.stringify(value, null, 2) ?? String(value); + } catch { + return String(value); + } +}; + +export const JsonInspector = ({ + className, + collectionLimit, + copyable = false, + data, + hideRoot, + shouldExpandNodeInitially, + sortObjectKeys, + theme = 'dark', +}: JsonInspectorProps) => { + const { copy, isCopied } = useCopyToClipboard(); + const clipboardValue = useMemo(() => stringifyForClipboard(data), [data]); + + return ( +
+ {copyable ? ( + + ) : null} +
+ +
+
+ ); +}; diff --git a/packages/ui/src/utils/cn.ts b/packages/ui/src/utils/cn.ts new file mode 100644 index 00000000..7f3a63bd --- /dev/null +++ b/packages/ui/src/utils/cn.ts @@ -0,0 +1,2 @@ +export const cn = (...classNames: Array) => + classNames.filter(Boolean).join(' '); diff --git a/packages/ui/src/utils/json.ts b/packages/ui/src/utils/json.ts new file mode 100644 index 00000000..f5d8cde1 --- /dev/null +++ b/packages/ui/src/utils/json.ts @@ -0,0 +1,38 @@ +export type JsonInspectionParseMode = 'any' | 'object-or-array'; + +export type JsonInspectionParseResult = + | { + ok: true; + value: unknown; + } + | { + ok: false; + value: null; + }; + +const isInspectableObjectOrArray = ( + value: unknown, +): value is Record | unknown[] => { + if (Array.isArray(value)) { + return true; + } + + return value !== null && typeof value === 'object'; +}; + +export const parseJsonForInspection = ( + value: string, + mode: JsonInspectionParseMode = 'object-or-array', +): JsonInspectionParseResult => { + try { + const parsed = JSON.parse(value) as unknown; + + if (mode === 'object-or-array' && !isInspectableObjectOrArray(parsed)) { + return { ok: false, value: null }; + } + + return { ok: true, value: parsed }; + } catch { + return { ok: false, value: null }; + } +}; diff --git a/packages/ui/styles.css b/packages/ui/styles.css new file mode 100644 index 00000000..04a645ac --- /dev/null +++ b/packages/ui/styles.css @@ -0,0 +1,97 @@ +@import 'tailwindcss'; +@import '@heroui/styles'; + +@theme inline { + --color-info: var(--info); + --color-info-foreground: var(--info-foreground); + --color-muted: var(--muted); + --color-overlay: var(--overlay); + --color-overlay-foreground: var(--overlay-foreground); + --color-segment: var(--segment); + --color-segment-foreground: var(--segment-foreground); + --color-surface-secondary: var(--surface-secondary); + --color-surface-secondary-foreground: var(--surface-secondary-foreground); + --color-surface-tertiary: var(--surface-tertiary); + --color-surface-tertiary-foreground: var(--surface-tertiary-foreground); + --color-field-background: var(--field-background); + --color-field-foreground: var(--field-foreground); + --color-field-placeholder: var(--field-placeholder); +} + +:root, +.light, +.default, +[data-theme='light'], +[data-theme='default'] { + --accent: oklch(55.59% 0.2717 292.75); + --accent-foreground: oklch(99.11% 0 0); + --background: oklch(97.02% 0.0011 292.75); + --border: oklch(90% 0.0011 292.75); + --danger: oklch(65.32% 0.2333 30.41); + --danger-foreground: oklch(99.11% 0 0); + --default: oklch(94% 0.0011 292.75); + --default-foreground: oklch(21.03% 0.0059 292.75); + --field-background: oklch(100% 0.0006 292.75); + --field-foreground: oklch(21.03% 0.0011 292.75); + --field-placeholder: oklch(55.17% 0.0022 292.75); + --focus: oklch(55.59% 0.2717 292.75); + --foreground: oklch(21.03% 0.0011 292.75); + --muted: oklch(55.17% 0.0022 292.75); + --overlay: oklch(100% 0.0003 292.75); + --overlay-foreground: oklch(21.03% 0.0011 292.75); + --scrollbar: oklch(87.1% 0.0011 292.75); + --segment: oklch(100% 0.0011 292.75); + --segment-foreground: oklch(21.03% 0.0011 292.75); + --separator: oklch(92% 0.0011 292.75); + --success: oklch(73.29% 0.1939 155.48); + --success-foreground: oklch(21.03% 0.0059 155.48); + --surface: oklch(100% 0.0006 292.75); + --surface-foreground: oklch(21.03% 0.0011 292.75); + --surface-secondary: oklch(95.24% 0.0009 292.75); + --surface-secondary-foreground: oklch(21.03% 0.0011 292.75); + --surface-tertiary: oklch(93.73% 0.0009 292.75); + --surface-tertiary-foreground: oklch(21.03% 0.0011 292.75); + --warning: oklch(78.19% 0.1588 77); + --warning-foreground: oklch(21.03% 0.0059 77); + + --radius: 0.25rem; + --field-radius: 0.25rem; + + /*--font-sans: var(--font-figtree);*/ +} + +.dark, +[data-theme='dark'] { + color-scheme: dark; + /* Theme Colors (Dark Mode) */ + --accent: oklch(55.59% 0.2717 292.75); + --accent-foreground: oklch(99.11% 0 0); + --background: oklch(12% 0.0011 292.75); + --border: oklch(28% 0.0011 292.75); + --danger: oklch(59.4% 0.1971 29.3); + --danger-foreground: oklch(99.11% 0 0); + --default: oklch(27.4% 0.0011 292.75); + --default-foreground: oklch(99.11% 0 0); + --field-background: oklch(21.03% 0.0022 292.75); + --field-foreground: oklch(99.11% 0.0011 292.75); + --field-placeholder: oklch(70.5% 0.0022 292.75); + --focus: oklch(55.59% 0.2717 292.75); + --foreground: oklch(99.11% 0.0011 292.75); + --muted: oklch(70.5% 0.0022 292.75); + --overlay: oklch(21.03% 0.0022 292.75); + --overlay-foreground: oklch(99.11% 0.0011 292.75); + --scrollbar: oklch(70.5% 0.0011 292.75); + --segment: oklch(39.64% 0.0011 292.75); + --segment-foreground: oklch(99.11% 0.0011 292.75); + --separator: oklch(25% 0.0011 292.75); + --success: oklch(73.29% 0.1939 155.48); + --success-foreground: oklch(21.03% 0.0059 155.48); + --surface: oklch(21.03% 0.0022 292.75); + --surface-foreground: oklch(99.11% 0.0011 292.75); + --surface-secondary: oklch(25.7% 0.0016 292.75); + --surface-secondary-foreground: oklch(99.11% 0.0011 292.75); + --surface-tertiary: oklch(27.21% 0.0016 292.75); + --surface-tertiary-foreground: oklch(99.11% 0.0011 292.75); + --warning: oklch(82.03% 0.1391 81.01); + --warning-foreground: oklch(21.03% 0.0059 81.01); +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 00000000..c23e61c8 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/ui/tsconfig.lib.json b/packages/ui/tsconfig.lib.json new file mode 100644 index 00000000..e23a04be --- /dev/null +++ b/packages/ui/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "vite/client"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts new file mode 100644 index 00000000..49c5624c --- /dev/null +++ b/packages/ui/vite.config.ts @@ -0,0 +1,41 @@ +/// +import path, { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +const external = [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'tslib', + /^@heroui\//, + /^@radix-ui\/react-tabs$/, + /^@tanstack\/react-table$/, + /^lucide-react$/, + /^react-json-tree$/, + /^react-virtuoso$/, + /^tailwind-variants$/, +]; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/ui', + base: './', + plugins: [ + dts({ + entryRoot: 'src', + tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), + rollupTypes: true, + }), + ], + build: { + ssr: true, + lib: { + entry: resolve(__dirname, 'src/index.ts'), + formats: ['es', 'cjs'], + }, + rollupOptions: { + external, + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06c0a8c1..d9d4f3c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -652,7 +652,7 @@ importers: version: 7.7.0 vitest: specifier: ^4.0.18 - version: 4.1.0(@types/node@22.17.0)(@vitest/ui@3.2.4(vitest@3.2.4))(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + version: 4.1.0(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) packages/mmkv-plugin: dependencies: @@ -662,6 +662,9 @@ importers: '@rozenite/plugin-bridge': specifier: workspace:* version: link:../plugin-bridge + '@rozenite/ui': + specifier: workspace:* + version: link:../ui nanoevents: specifier: ^9.1.0 version: 9.1.0 @@ -669,6 +672,9 @@ importers: '@rozenite/vite-plugin': specifier: workspace:* version: link:../vite-plugin + '@tailwindcss/postcss': + specifier: ^4.2.2 + version: 4.2.2 '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -690,9 +696,6 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.0(react@19.2.0) - react-json-tree: - specifier: ^0.20.0 - version: 0.20.0(@types/react@19.2.14)(react@19.2.0) react-native: specifier: 'catalog:' version: 0.83.1(@babel/core@7.29.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) @@ -714,12 +717,15 @@ importers: rozenite: specifier: workspace:* version: link:../cli + tailwind-variants: + specifier: ^2.0.0 + version: 2.1.0(tailwind-merge@3.4.0)(tailwindcss@4.2.2) tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 + specifier: ^4.2.2 + version: 4.2.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.17) + version: 1.0.7(tailwindcss@4.2.2) typescript: specifier: ~5.9.3 version: 5.9.3 @@ -1310,10 +1316,16 @@ importers: '@rozenite/plugin-bridge': specifier: workspace:* version: link:../plugin-bridge + '@rozenite/ui': + specifier: workspace:* + version: link:../ui devDependencies: '@rozenite/vite-plugin': specifier: workspace:* version: link:../vite-plugin + '@tailwindcss/postcss': + specifier: ^4.2.2 + version: 4.2.2 '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -1335,9 +1347,6 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.0(react@19.2.0) - react-json-tree: - specifier: ^0.20.0 - version: 0.20.0(@types/react@19.2.14)(react@19.2.0) react-native: specifier: 'catalog:' version: 0.83.1(@babel/core@7.29.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0) @@ -1359,12 +1368,15 @@ importers: rozenite: specifier: workspace:* version: link:../cli + tailwind-variants: + specifier: ^2.0.0 + version: 2.1.0(tailwind-merge@3.4.0)(tailwindcss@4.2.2) tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 + specifier: ^4.2.2 + version: 4.2.2 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.17) + version: 1.0.7(tailwindcss@4.2.2) typescript: specifier: ~5.9.3 version: 5.9.3 @@ -1442,6 +1454,55 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + packages/ui: + dependencies: + '@heroui/react': + specifier: ^3.0.1 + version: 3.0.1(@types/react-dom@19.1.11(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(tailwindcss@4.2.2) + '@heroui/styles': + specifier: ^3.0.1 + version: 3.0.1(tailwind-merge@3.4.0)(tailwindcss@4.2.2) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + lucide-react: + specifier: ^0.263.1 + version: 0.263.1(react@19.2.0) + react-json-tree: + specifier: ^0.20.0 + version: 0.20.0(@types/react@19.2.14)(react@19.2.0) + react-virtuoso: + specifier: ^4.14.1 + version: 4.18.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + tailwind-variants: + specifier: ^2.0.0 + version: 2.1.0(tailwind-merge@3.4.0)(tailwindcss@4.2.2) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + tslib: + specifier: ^2.3.0 + version: 2.8.1 + devDependencies: + '@types/react': + specifier: 'catalog:' + version: 19.2.14 + '@types/react-dom': + specifier: 'catalog:' + version: 19.1.11(@types/react@19.2.14) + react: + specifier: 'catalog:' + version: 19.2.0 + react-dom: + specifier: 'catalog:' + version: 19.2.0(react@19.2.0) + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + packages/vite-plugin: dependencies: '@microsoft/api-extractor': @@ -1520,10 +1581,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1540,10 +1597,6 @@ packages: resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} - engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} @@ -1657,10 +1710,6 @@ packages: resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} - engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} @@ -6255,6 +6304,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -6819,9 +6869,6 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001750: - resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} - caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} @@ -11212,6 +11259,12 @@ packages: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' + react-virtuoso@4.18.5: + resolution: {integrity: sha512-QDyNjyNEuurZG67SOmzYyxEkQYSyGmAMixOI6M15L/Q4CF39EgG+88y6DgZRo0q7rmy0HPx3Fj90I8/tPdnRCQ==} + peerDependencies: + react: '>=16 || >=17 || >= 18 || >= 19' + react-dom: '>=16 || >=17 || >= 18 || >=19' + react-window@1.8.11: resolution: {integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==} engines: {node: '>8.0.0'} @@ -12001,6 +12054,16 @@ packages: tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + tailwind-variants@2.1.0: + resolution: {integrity: sha512-82m0eRex0z6A3GpvfoTCpHr+wWJmbecfVZfP3mqLoDxeya5tN4mYJQZwa5Aw1hRZTedwpu1D2JizYenoEdyD8w==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + tailwind-variants@3.2.2: resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} engines: {node: '>=16.x', pnpm: '>=7.x'} @@ -13004,11 +13067,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -13025,26 +13083,6 @@ snapshots: '@babel/compat-data@7.29.0': {} - '@babel/core@7.28.0': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - '@babel/types': 7.29.0 - convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@5.5.0) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 @@ -13081,7 +13119,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.3 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -13093,19 +13131,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13119,13 +13144,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.2.0 - semver: 6.3.1 - '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13133,17 +13151,6 @@ snapshots: regexpu-core: 6.2.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.28.6 - debug: 4.4.3(supports-color@5.5.0) - lodash.debounce: 4.0.8 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13178,15 +13185,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13213,15 +13211,6 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.27.1 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13231,15 +13220,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.28.6(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13272,11 +13252,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helpers@7.27.6': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.28.6 @@ -13286,10 +13261,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.28.0)': + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.0) + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -13303,28 +13278,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.28.0)': + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.28.0)': + '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) transitivePeerDependencies: - supports-color @@ -13353,31 +13323,16 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-export-default-from@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-export-default-from@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13398,11 +13353,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13413,11 +13363,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13438,11 +13383,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13458,35 +13398,16 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13496,15 +13417,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13514,24 +13426,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13548,18 +13447,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.28.0) - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-classes@7.28.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13572,26 +13459,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/template': 7.28.6 - '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/template': 7.28.6 - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13605,26 +13478,12 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13633,15 +13492,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13651,34 +13501,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13687,49 +13519,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/traverse': 7.29.0(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13741,24 +13546,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13767,24 +13559,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13793,15 +13572,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13811,11 +13581,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13828,37 +13593,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13876,28 +13620,11 @@ snapshots: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regenerator@7.28.4(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1(supports-color@5.5.0) - '@babel/helper-plugin-utils': 7.28.6 - babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) - babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13910,24 +13637,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-spread@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13936,11 +13650,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13951,17 +13660,6 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -13973,24 +13671,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 - '@babel/preset-flow@7.27.1(@babel/core@7.28.0)': + '@babel/preset-flow@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) '@babel/preset-react@7.28.5(@babel/core@7.29.0)': dependencies: @@ -14004,17 +13696,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -14026,9 +13707,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/register@7.27.1(@babel/core@7.28.0)': + '@babel/register@7.27.1(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -17508,50 +17189,50 @@ snapshots: '@react-native/babel-preset@0.76.0': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.0) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.0) - '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.29.0 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.28.6 '@react-native/babel-plugin-codegen': 0.76.0 babel-plugin-syntax-hermes-parser: 0.23.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.0) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) react-refresh: 0.14.2 transitivePeerDependencies: - '@babel/preset-env' @@ -17559,50 +17240,50 @@ snapshots: '@react-native/babel-preset@0.76.9': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.28.0) - '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) - '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.28.0) - '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.29.0 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.4(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.28.4(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) '@babel/template': 7.28.6 '@react-native/babel-plugin-codegen': 0.76.9 babel-plugin-syntax-hermes-parser: 0.25.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.28.0) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) react-refresh: 0.14.2 transitivePeerDependencies: - '@babel/preset-env' @@ -17835,7 +17516,7 @@ snapshots: '@react-native/metro-babel-transformer@0.76.0': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@react-native/babel-preset': 0.76.0 hermes-parser: 0.23.1 nullthrows: 1.1.1 @@ -17845,7 +17526,7 @@ snapshots: '@react-native/metro-babel-transformer@0.76.9': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@react-native/babel-preset': 0.76.9 hermes-parser: 0.23.1 nullthrows: 1.1.1 @@ -19907,9 +19588,9 @@ snapshots: '@vitejs/plugin-react@4.6.0(vite@7.3.1(@types/node@18.16.9)(jiti@2.4.2)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@rolldown/pluginutils': 1.0.0-beta.19 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 @@ -19919,9 +19600,9 @@ snapshots: '@vitejs/plugin-react@4.6.0(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) '@rolldown/pluginutils': 1.0.0-beta.19 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 @@ -20032,7 +19713,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@18.16.9)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -20445,8 +20126,8 @@ snapshots: autoprefixer@10.4.21(postcss@8.5.6): dependencies: - browserslist: 4.26.3 - caniuse-lite: 1.0.30001750 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001787 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -20464,9 +20145,9 @@ snapshots: await-lock@2.2.2: {} - babel-core@7.0.0-bridge.0(@babel/core@7.28.0): + babel-core@7.0.0-bridge.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 babel-jest@29.7.0(@babel/core@7.29.0): dependencies: @@ -20528,15 +20209,6 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.10 - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0): - dependencies: - '@babel/compat-data': 7.28.0 - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.29.0): dependencies: '@babel/compat-data': 7.28.0 @@ -20546,14 +20218,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - core-js-compat: 3.43.0 - transitivePeerDependencies: - - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -20562,13 +20226,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 - '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) - transitivePeerDependencies: - - supports-color - babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -20614,12 +20271,6 @@ snapshots: dependencies: hermes-parser: 0.33.3 - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.28.0): - dependencies: - '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.28.0) - transitivePeerDependencies: - - '@babel/core' - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): dependencies: '@babel/plugin-syntax-flow': 7.27.1(@babel/core@7.29.0) @@ -20787,7 +20438,7 @@ snapshots: browserslist@4.26.3: dependencies: baseline-browser-mapping: 2.8.16 - caniuse-lite: 1.0.30001750 + caniuse-lite: 1.0.30001787 electron-to-chromium: 1.5.234 node-releases: 2.0.23 update-browserslist-db: 1.1.3(browserslist@4.26.3) @@ -20870,8 +20521,6 @@ snapshots: camelize@1.0.1: {} - caniuse-lite@1.0.30001750: {} - caniuse-lite@1.0.30001787: {} ccount@2.0.1: {} @@ -21187,7 +20836,7 @@ snapshots: core-js-compat@3.43.0: dependencies: - browserslist: 4.26.3 + browserslist: 4.28.2 core-util-is@1.0.3: {} @@ -23819,16 +23468,16 @@ snapshots: jscodeshift@0.14.0: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/parser': 7.29.0 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.28.0) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.28.0) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.28.0) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) - '@babel/preset-flow': 7.27.1(@babel/core@7.28.0) - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) - '@babel/register': 7.27.1(@babel/core@7.28.0) - babel-core: 7.0.0-bridge.0(@babel/core@7.28.0) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.29.0) + '@babel/preset-flow': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.29.0) + '@babel/register': 7.27.1(@babel/core@7.29.0) + babel-core: 7.0.0-bridge.0(@babel/core@7.29.0) chalk: 4.1.2 flow-parser: 0.274.2 graceful-fs: 4.2.11 @@ -24489,7 +24138,7 @@ snapshots: metro-babel-transformer@0.81.5: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 flow-enums-runtime: 0.0.6 hermes-parser: 0.25.1 nullthrows: 1.1.1 @@ -24498,7 +24147,7 @@ snapshots: metro-babel-transformer@0.82.5: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 flow-enums-runtime: 0.0.6 hermes-parser: 0.29.1 nullthrows: 1.1.1 @@ -24777,7 +24426,7 @@ snapshots: metro-transform-plugins@0.81.5: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0(supports-color@5.5.0) @@ -24788,7 +24437,7 @@ snapshots: metro-transform-plugins@0.82.5: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0(supports-color@5.5.0) @@ -24810,7 +24459,7 @@ snapshots: metro-transform-worker@0.81.5: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 '@babel/types': 7.29.0 @@ -24830,7 +24479,7 @@ snapshots: metro-transform-worker@0.82.5: dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 '@babel/types': 7.29.0 @@ -24871,7 +24520,7 @@ snapshots: metro@0.81.5: dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 '@babel/template': 7.28.6 @@ -24918,7 +24567,7 @@ snapshots: metro@0.82.5: dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 '@babel/template': 7.28.6 @@ -26698,6 +26347,11 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + react-virtuoso@4.18.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-window@1.8.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@babel/runtime': 7.26.10 @@ -27655,6 +27309,12 @@ snapshots: tailwind-merge@3.4.0: {} + tailwind-variants@2.1.0(tailwind-merge@3.4.0)(tailwindcss@4.2.2): + dependencies: + tailwindcss: 4.2.2 + optionalDependencies: + tailwind-merge: 3.4.0 + tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.2.2): dependencies: tailwindcss: 4.2.2 @@ -27665,6 +27325,10 @@ snapshots: dependencies: tailwindcss: 3.4.17 + tailwindcss-animate@1.0.7(tailwindcss@4.2.2): + dependencies: + tailwindcss: 4.2.2 + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -28431,7 +28095,7 @@ snapshots: - tsx - yaml - vitest@4.1.0(@types/node@22.17.0)(@vitest/ui@3.2.4(vitest@3.2.4))(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1): + vitest@4.1.0(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: '@vitest/expect': 4.1.0 '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)) diff --git a/tsconfig.base.json b/tsconfig.base.json index 80848320..cae74a38 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,6 +1,11 @@ { "compilerOptions": { + "baseUrl": ".", "composite": true, + "paths": { + "@rozenite/ui": ["packages/ui/src/index.ts"], + "@rozenite/ui/*": ["packages/ui/*"] + }, "declarationMap": true, "emitDeclarationOnly": true, "importHelpers": true, From 591bbfca1d39fcf469515a991cf4b8ee9ed65097 Mon Sep 17 00:00:00 2001 From: Anatoly Belobrovik Date: Thu, 30 Apr 2026 16:40:16 +0200 Subject: [PATCH 2/3] feat: unify ui --- packages/controls-plugin/package.json | 6 +- packages/controls-plugin/postcss.config.js | 3 +- packages/controls-plugin/src/ui/globals.css | 88 +--- packages/controls-plugin/src/ui/panel.tsx | 247 ++++++--- packages/controls-plugin/tailwind.config.ts | 53 -- packages/file-system-plugin/package.json | 21 +- packages/file-system-plugin/postcss.config.js | 3 +- .../file-system-plugin/src/file-system.tsx | 330 +++++++----- packages/file-system-plugin/src/types.ts | 6 - .../src/ui/ConnectingScreen.tsx | 53 +- .../file-system-plugin/src/ui/DetailLine.tsx | 37 -- .../file-system-plugin/src/ui/DetailPanel.tsx | 380 +++++++------- .../src/ui/FileEntryRow.tsx | 86 --- .../src/ui/FileEntryTable.tsx | 319 +++++++++++ .../file-system-plugin/src/ui/PathDisplay.tsx | 190 ++----- packages/file-system-plugin/src/ui/TopBar.tsx | 244 --------- .../file-system-plugin/src/ui/globals.css | 2 + .../file-system-plugin/tailwind.config.ts | 94 ---- packages/file-system-plugin/tsconfig.json | 6 + .../mmkv-plugin/src/ui/editable-table.tsx | 20 +- packages/mmkv-plugin/src/ui/globals.css | 39 -- packages/mmkv-plugin/src/ui/panel.tsx | 98 ++-- packages/overlay-plugin/package.json | 8 +- packages/overlay-plugin/postcss.config.js | 4 +- .../src/ui/components/GridSettings.tsx | 326 ++++++++---- .../src/ui/components/Header.tsx | 14 - .../src/ui/components/ImageSettings.tsx | 402 +++++++++----- .../overlay-plugin/src/ui/components/index.ts | 1 - packages/overlay-plugin/src/ui/globals.css | 98 +--- packages/overlay-plugin/src/ui/panel.tsx | 57 +- packages/overlay-plugin/src/ui/styles.css | 330 ------------ packages/overlay-plugin/tailwind.config.ts | 95 ---- .../performance-monitor-plugin/package.json | 12 +- .../postcss.config.js | 5 + .../performance-monitor-plugin/src/ui/App.css | 97 ---- .../performance-monitor-plugin/src/ui/App.tsx | 263 ++++------ .../src/ui/components/DataTable.tsx | 223 +++++--- .../src/ui/components/DetailsDisplay.tsx | 93 +++- .../src/ui/components/DetailsSidebar.tsx | 103 ++-- .../src/ui/components/ExportModal.tsx | 338 ++++++------ .../src/ui/components/JsonTree.tsx | 35 -- .../src/ui/components/MarkDetails.tsx | 52 +- .../src/ui/components/MarksTable.tsx | 35 +- .../src/ui/components/MeasureDetails.tsx | 77 +-- .../src/ui/components/MeasuresTable.tsx | 61 +-- .../src/ui/components/MetricDetails.tsx | 81 ++- .../src/ui/components/MetricsTable.tsx | 52 +- .../src/ui/components/SessionDuration.tsx | 55 +- .../src/ui/globals.css | 2 + .../src/ui/utils.ts | 94 ++++ packages/react-navigation-plugin/package.json | 7 +- .../react-navigation-plugin/postcss.config.js | 3 +- .../components/ActionDetailPanel.tsx | 89 ++-- .../src/devtools-ui/components/ActionItem.tsx | 107 ++-- .../src/devtools-ui/components/ActionList.tsx | 13 +- .../devtools-ui/components/ActionSidebar.tsx | 67 +-- .../devtools-ui/components/ActionTimeline.tsx | 41 +- .../devtools-ui/components/LinkingTester.tsx | 69 ++- .../components/NavigationTree/Leaf.tsx | 32 +- .../NavigationTree/NavigationNode.tsx | 53 +- .../NavigationTree/NavigationTree.tsx | 22 +- .../NavigationTree/navigationTreeColors.ts | 133 +++-- .../src/devtools-ui/components/Tabs.tsx | 41 -- .../src/devtools-ui/globals.css | 125 +---- .../src/devtools-ui/index.tsx | 63 ++- .../tailwind.config.ts | 103 ---- .../storage-plugin/src/ui/editable-table.tsx | 20 +- packages/storage-plugin/src/ui/globals.css | 39 -- packages/storage-plugin/src/ui/panel.tsx | 100 ++-- packages/ui/src/index.ts | 14 + packages/ui/src/plugin-header.tsx | 114 ++++ packages/ui/src/plugin-theme.tsx | 236 +++++++++ packages/ui/styles.css | 56 +- pnpm-lock.yaml | 494 ++++-------------- 74 files changed, 3427 insertions(+), 3952 deletions(-) delete mode 100644 packages/controls-plugin/tailwind.config.ts delete mode 100644 packages/file-system-plugin/src/types.ts delete mode 100644 packages/file-system-plugin/src/ui/DetailLine.tsx delete mode 100644 packages/file-system-plugin/src/ui/FileEntryRow.tsx create mode 100644 packages/file-system-plugin/src/ui/FileEntryTable.tsx delete mode 100644 packages/file-system-plugin/src/ui/TopBar.tsx create mode 100644 packages/file-system-plugin/src/ui/globals.css delete mode 100644 packages/file-system-plugin/tailwind.config.ts delete mode 100644 packages/overlay-plugin/src/ui/components/Header.tsx delete mode 100644 packages/overlay-plugin/src/ui/styles.css delete mode 100644 packages/overlay-plugin/tailwind.config.ts create mode 100644 packages/performance-monitor-plugin/postcss.config.js delete mode 100644 packages/performance-monitor-plugin/src/ui/App.css delete mode 100644 packages/performance-monitor-plugin/src/ui/components/JsonTree.tsx create mode 100644 packages/performance-monitor-plugin/src/ui/globals.css delete mode 100644 packages/react-navigation-plugin/src/devtools-ui/components/Tabs.tsx delete mode 100644 packages/react-navigation-plugin/tailwind.config.ts create mode 100644 packages/ui/src/plugin-header.tsx create mode 100644 packages/ui/src/plugin-theme.tsx diff --git a/packages/controls-plugin/package.json b/packages/controls-plugin/package.json index 468fbd3f..a9222995 100644 --- a/packages/controls-plugin/package.json +++ b/packages/controls-plugin/package.json @@ -22,10 +22,12 @@ }, "dependencies": { "@rozenite/agent-bridge": "workspace:*", - "@rozenite/plugin-bridge": "workspace:*" + "@rozenite/plugin-bridge": "workspace:*", + "@rozenite/ui": "workspace:*" }, "devDependencies": { "@rozenite/vite-plugin": "workspace:*", + "@tailwindcss/postcss": "^4.2.2", "@types/react": "catalog:", "@types/react-dom": "catalog:", "autoprefixer": "^10.4.21", @@ -35,7 +37,7 @@ "react-native": "catalog:", "react-native-web": "^0.21.2", "rozenite": "workspace:*", - "tailwindcss": "^3.4.17", + "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", "typescript": "~5.9.3", "vite": "catalog:" diff --git a/packages/controls-plugin/postcss.config.js b/packages/controls-plugin/postcss.config.js index 2aa7205d..a34a3d56 100644 --- a/packages/controls-plugin/postcss.config.js +++ b/packages/controls-plugin/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/packages/controls-plugin/src/ui/globals.css b/packages/controls-plugin/src/ui/globals.css index c42d28c0..e862ec52 100644 --- a/packages/controls-plugin/src/ui/globals.css +++ b/packages/controls-plugin/src/ui/globals.css @@ -1,86 +1,2 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --radius: 0.5rem; - } - - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - } - - * { - @apply border-border; - } - - body { - @apply bg-background text-foreground; - } -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: #1f2937; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb { - background: #374151; - border-radius: 4px; - border: 1px solid #1f2937; -} - -::-webkit-scrollbar-thumb:hover { - background: #4b5563; -} - -::-webkit-scrollbar-thumb:active { - background: #6b7280; -} - -::-webkit-scrollbar-corner { - background: #1f2937; -} +@import 'tailwindcss'; +@import '@rozenite/ui/styles.css'; diff --git a/packages/controls-plugin/src/ui/panel.tsx b/packages/controls-plugin/src/ui/panel.tsx index 5f180a2d..7a8963ed 100644 --- a/packages/controls-plugin/src/ui/panel.tsx +++ b/packages/controls-plugin/src/ui/panel.tsx @@ -1,4 +1,19 @@ import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge'; +import { + Button, + Card, + Chip, + Description, + FieldError, + Input, + ListBox, + PluginHeader, + PluginTheme, + Select, + Surface, + Switch, + TextField, +} from '@rozenite/ui'; import type { ReactNode } from 'react'; import { useEffect, useRef, useState } from 'react'; import type { @@ -6,7 +21,10 @@ import type { ControlsSnapshotEvent, ControlsUpdateResultEvent, } from '../shared/messaging'; -import type { ControlsItemSnapshot, ControlsSectionSnapshot } from '../shared/types'; +import type { + ControlsItemSnapshot, + ControlsSectionSnapshot, +} from '../shared/types'; import './globals.css'; type ItemUiState = { @@ -14,7 +32,8 @@ type ItemUiState = { message?: string; }; -const getItemKey = (sectionId: string, itemId: string) => `${sectionId}:${itemId}`; +const getItemKey = (sectionId: string, itemId: string) => + `${sectionId}:${itemId}`; const createRequestId = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; @@ -30,17 +49,21 @@ const RowShell = ({ errorMessage?: string; children: ReactNode; }) => ( -
+
-
{title}
+
{title}
{description ? ( -
{description}
+ + {description} + ) : null} {errorMessage ? ( -
{errorMessage}
+ {errorMessage} ) : null}
- {children} +
+ {children} +
); @@ -61,16 +84,17 @@ const ToggleRow = ({ description={item.description} errorMessage={uiState?.message} > -