mirror of
https://github.com/stackblitz-labs/bolt.diy.git
synced 2025-06-18 01:49:07 +01:00
beta New control panel
# Tab Management System Implementation ## What's Been Implemented 1. Complete Tab Management System with: - Drag and drop functionality for reordering tabs - Visual feedback during drag operations - Smooth animations and transitions - Dark mode support - Search functionality for tabs - Reset to defaults option 2. Developer Mode Features: - Shows ALL available tabs in developer mode - Maintains tab order across modes - Proper visibility toggles - Automatic inclusion of developer-specific tabs 3. User Mode Features: - Shows only user-configured tabs - Maintains separate tab configurations - Proper visibility management ## Key Components - `TabManagement.tsx`: Main management interface - `ControlPanel.tsx`: Main panel with tab display - Integration with tab configuration store - Proper type definitions and interfaces ## Technical Features - React DnD for drag and drop - Framer Motion for animations - TypeScript for type safety - UnoCSS for styling - Toast notifications for user feedback ## Next Steps 1. Testing: - Test tab visibility in both modes - Verify drag and drop persistence - Check dark mode compatibility - Verify search functionality - Test reset functionality 2. Potential Improvements: - Add tab grouping functionality - Implement tab pinning - Add keyboard shortcuts - Improve accessibility - Add tab descriptions - Add tab icons customization 3. Documentation: - Add inline code comments - Create user documentation - Document API interfaces - Add setup instructions 4. Future Features: - Tab export/import - Custom tab creation - Tab templates - User preferences sync - Tab statistics ## Known Issues to Address 1. Ensure all tabs are visible in developer mode 2. Improve drag and drop performance 3. Better state persistence 4. Enhanced error handling 5. Improved type safety ## Usage Instructions 1. Switch to developer mode to see all available tabs 2. Use drag and drop to reorder tabs 3. Toggle visibility using switches 4. Use search to filter tabs 5. Reset to defaults if needed ## Technical Debt 1. Refactor tab configuration store 2. Improve type definitions 3. Add proper error boundaries 4. Implement proper loading states 5. Add comprehensive testing ## Security Considerations 1. Validate tab configurations 2. Sanitize user input 3. Implement proper access control 4. Add audit logging 5. Secure state management
This commit is contained in:
parent
af620d0197
commit
999d87b1e8
607
app/components/settings/ControlPanel.tsx
Normal file
607
app/components/settings/ControlPanel.tsx
Normal file
@ -0,0 +1,607 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Switch } from '@radix-ui/react-switch';
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { TabManagement } from './developer/TabManagement';
|
||||
import { TabTile } from './shared/TabTile';
|
||||
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
||||
import { useFeatures } from '~/lib/hooks/useFeatures';
|
||||
import { useNotifications } from '~/lib/hooks/useNotifications';
|
||||
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
||||
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
||||
import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
|
||||
import type { TabType, TabVisibilityConfig } from './settings.types';
|
||||
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './settings.types';
|
||||
import { resetTabConfiguration } from '~/lib/stores/settings';
|
||||
import { DialogTitle } from '~/components/ui/Dialog';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
|
||||
// Import all tab components
|
||||
import ProfileTab from './profile/ProfileTab';
|
||||
import SettingsTab from './settings/SettingsTab';
|
||||
import NotificationsTab from './notifications/NotificationsTab';
|
||||
import FeaturesTab from './features/FeaturesTab';
|
||||
import DataTab from './data/DataTab';
|
||||
import DebugTab from './debug/DebugTab';
|
||||
import { EventLogsTab } from './event-logs/EventLogsTab';
|
||||
import UpdateTab from './update/UpdateTab';
|
||||
import ConnectionsTab from './connections/ConnectionsTab';
|
||||
import CloudProvidersTab from './providers/CloudProvidersTab';
|
||||
import ServiceStatusTab from './providers/ServiceStatusTab';
|
||||
import LocalProvidersTab from './providers/LocalProvidersTab';
|
||||
import TaskManagerTab from './task-manager/TaskManagerTab';
|
||||
|
||||
interface ControlPanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface TabWithDevType extends TabVisibilityConfig {
|
||||
isExtraDevTab?: boolean;
|
||||
}
|
||||
|
||||
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
profile: 'Manage your profile and account settings',
|
||||
settings: 'Configure application preferences',
|
||||
notifications: 'View and manage your notifications',
|
||||
features: 'Explore new and upcoming features',
|
||||
data: 'Manage your data and storage',
|
||||
'cloud-providers': 'Configure cloud AI providers and models',
|
||||
'local-providers': 'Configure local AI providers and models',
|
||||
'service-status': 'Monitor cloud LLM service status',
|
||||
connection: 'Check connection status and settings',
|
||||
debug: 'Debug tools and system information',
|
||||
'event-logs': 'View system events and logs',
|
||||
update: 'Check for updates and release notes',
|
||||
'task-manager': 'Monitor system resources and processes',
|
||||
};
|
||||
|
||||
// Add DraggableTabTile component before the ControlPanel component
|
||||
const DraggableTabTile = ({
|
||||
tab,
|
||||
index,
|
||||
moveTab,
|
||||
...props
|
||||
}: {
|
||||
tab: TabWithDevType;
|
||||
index: number;
|
||||
moveTab: (dragIndex: number, hoverIndex: number) => void;
|
||||
onClick: () => void;
|
||||
isActive: boolean;
|
||||
hasUpdate: boolean;
|
||||
statusMessage: string;
|
||||
description: string;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'tab',
|
||||
item: { index, id: tab.id },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: 'tab',
|
||||
hover: (item: { index: number; id: string }, monitor) => {
|
||||
if (!monitor.isOver({ shallow: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.id === tab.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.index === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only move when hovering over the middle section
|
||||
const hoverBoundingRect = monitor.getSourceClientOffset();
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
|
||||
if (!hoverBoundingRect || !clientOffset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoverMiddleX = hoverBoundingRect.x + 150; // Half of typical card width
|
||||
const hoverClientX = clientOffset.x;
|
||||
|
||||
// Only perform the move when the mouse has crossed half of the items width
|
||||
if (item.index < index && hoverClientX < hoverMiddleX) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.index > index && hoverClientX > hoverMiddleX) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveTab(item.index, index);
|
||||
item.index = index;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver({ shallow: true }),
|
||||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
});
|
||||
|
||||
const dropIndicatorClasses = classNames('rounded-xl border-2 border-transparent transition-all duration-200', {
|
||||
'ring-2 ring-purple-500 ring-opacity-50 bg-purple-50 dark:bg-purple-900/20': isOver,
|
||||
'hover:ring-2 hover:ring-purple-500/30': canDrop && !isOver,
|
||||
});
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={(node) => drag(drop(node))}
|
||||
style={{
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
cursor: 'move',
|
||||
position: 'relative',
|
||||
zIndex: isDragging ? 100 : isOver ? 50 : 1,
|
||||
}}
|
||||
animate={{
|
||||
scale: isDragging ? 1.02 : isOver ? 1.05 : 1,
|
||||
boxShadow: isDragging
|
||||
? '0 8px 24px rgba(0, 0, 0, 0.15)'
|
||||
: isOver
|
||||
? '0 4px 12px rgba(147, 51, 234, 0.3)'
|
||||
: '0 0 0 rgba(0, 0, 0, 0)',
|
||||
borderColor: isOver ? 'rgb(147, 51, 234)' : isDragging ? 'rgba(147, 51, 234, 0.5)' : 'transparent',
|
||||
y: isOver ? -2 : 0,
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
mass: 0.8,
|
||||
}}
|
||||
className={dropIndicatorClasses}
|
||||
>
|
||||
<TabTile {...props} tab={tab} />
|
||||
{isOver && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-xl pointer-events-none"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-purple-500/10 to-purple-500/20 rounded-xl" />
|
||||
<div className="absolute inset-0 border-2 border-purple-500/50 rounded-xl" />
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
// State
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
||||
const [showTabManagement, setShowTabManagement] = useState(false);
|
||||
const [profile, setProfile] = useState({ avatar: null, notifications: true });
|
||||
|
||||
// Store values
|
||||
const tabConfiguration = useStore(tabConfigurationStore);
|
||||
const developerMode = useStore(developerModeStore);
|
||||
|
||||
// Status hooks
|
||||
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
||||
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
||||
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
||||
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
||||
|
||||
// Initialize profile from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = localStorage.getItem('bolt_user_profile');
|
||||
|
||||
if (saved) {
|
||||
try {
|
||||
const parsedProfile = JSON.parse(saved);
|
||||
setProfile(parsedProfile);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse profile from localStorage:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Add visibleTabs logic using useMemo
|
||||
const visibleTabs = useMemo(() => {
|
||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||
console.warn('Invalid tab configuration, resetting to defaults');
|
||||
resetTabConfiguration();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// In developer mode, show ALL tabs without restrictions
|
||||
if (developerMode) {
|
||||
// Combine all unique tabs from both user and developer configurations
|
||||
const allTabs = new Set([
|
||||
...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
|
||||
...tabConfiguration.userTabs.map((tab) => tab.id),
|
||||
...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
|
||||
]);
|
||||
|
||||
// Create a complete tab list with all tabs visible
|
||||
const devTabs = Array.from(allTabs).map((tabId) => {
|
||||
// Try to find existing configuration for this tab
|
||||
const existingTab =
|
||||
tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
|
||||
tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
|
||||
DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
|
||||
|
||||
return {
|
||||
id: tabId,
|
||||
visible: true,
|
||||
window: 'developer' as const,
|
||||
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
|
||||
};
|
||||
});
|
||||
|
||||
return devTabs.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
// In user mode, only show visible user tabs
|
||||
return tabConfiguration.userTabs
|
||||
.filter((tab) => {
|
||||
if (!tab || typeof tab.id !== 'string') {
|
||||
console.warn('Invalid tab entry:', tab);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show tabs that are explicitly visible and assigned to the user window
|
||||
return tab.visible && tab.window === 'user';
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}, [tabConfiguration, profile.notifications, developerMode]);
|
||||
|
||||
// Add moveTab handler
|
||||
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
||||
const newTabs = [...visibleTabs];
|
||||
const dragTab = newTabs[dragIndex];
|
||||
newTabs.splice(dragIndex, 1);
|
||||
newTabs.splice(hoverIndex, 0, dragTab);
|
||||
|
||||
// Update the order of the tabs
|
||||
const updatedTabs = newTabs.map((tab, index) => ({
|
||||
...tab,
|
||||
order: index,
|
||||
window: 'developer' as const,
|
||||
visible: true,
|
||||
}));
|
||||
|
||||
// Update the tab configuration store directly
|
||||
if (developerMode) {
|
||||
// In developer mode, update developerTabs while preserving configuration
|
||||
tabConfigurationStore.set({
|
||||
...tabConfiguration,
|
||||
developerTabs: updatedTabs,
|
||||
});
|
||||
} else {
|
||||
// In user mode, update userTabs
|
||||
tabConfigurationStore.set({
|
||||
...tabConfiguration,
|
||||
userTabs: updatedTabs.map((tab) => ({ ...tab, window: 'user' as const })),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handlers
|
||||
const handleBack = () => {
|
||||
if (showTabManagement) {
|
||||
setShowTabManagement(false);
|
||||
} else if (activeTab) {
|
||||
setActiveTab(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeveloperModeChange = (checked: boolean) => {
|
||||
console.log('Developer mode changed:', checked);
|
||||
setDeveloperMode(checked);
|
||||
};
|
||||
|
||||
// Add effect to log developer mode changes
|
||||
useEffect(() => {
|
||||
console.log('Current developer mode:', developerMode);
|
||||
}, [developerMode]);
|
||||
|
||||
const getTabComponent = () => {
|
||||
switch (activeTab) {
|
||||
case 'profile':
|
||||
return <ProfileTab />;
|
||||
case 'settings':
|
||||
return <SettingsTab />;
|
||||
case 'notifications':
|
||||
return <NotificationsTab />;
|
||||
case 'features':
|
||||
return <FeaturesTab />;
|
||||
case 'data':
|
||||
return <DataTab />;
|
||||
case 'cloud-providers':
|
||||
return <CloudProvidersTab />;
|
||||
case 'local-providers':
|
||||
return <LocalProvidersTab />;
|
||||
case 'connection':
|
||||
return <ConnectionsTab />;
|
||||
case 'debug':
|
||||
return <DebugTab />;
|
||||
case 'event-logs':
|
||||
return <EventLogsTab />;
|
||||
case 'update':
|
||||
return <UpdateTab />;
|
||||
case 'task-manager':
|
||||
return <TaskManagerTab />;
|
||||
case 'service-status':
|
||||
return <ServiceStatusTab />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
||||
switch (tabId) {
|
||||
case 'update':
|
||||
return hasUpdate;
|
||||
case 'features':
|
||||
return hasNewFeatures;
|
||||
case 'notifications':
|
||||
return hasUnreadNotifications;
|
||||
case 'connection':
|
||||
return hasConnectionIssues;
|
||||
case 'debug':
|
||||
return hasActiveWarnings;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusMessage = (tabId: TabType): string => {
|
||||
switch (tabId) {
|
||||
case 'update':
|
||||
return `New update available (v${currentVersion})`;
|
||||
case 'features':
|
||||
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
||||
case 'notifications':
|
||||
return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
|
||||
case 'connection':
|
||||
return currentIssue === 'disconnected'
|
||||
? 'Connection lost'
|
||||
: currentIssue === 'high-latency'
|
||||
? 'High latency detected'
|
||||
: 'Connection issues detected';
|
||||
case 'debug': {
|
||||
const warnings = activeIssues.filter((i) => i.type === 'warning').length;
|
||||
const errors = activeIssues.filter((i) => i.type === 'error').length;
|
||||
|
||||
return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabClick = (tabId: TabType) => {
|
||||
setLoadingTab(tabId);
|
||||
setActiveTab(tabId);
|
||||
|
||||
// Acknowledge notifications based on tab
|
||||
switch (tabId) {
|
||||
case 'update':
|
||||
acknowledgeUpdate();
|
||||
break;
|
||||
case 'features':
|
||||
acknowledgeAllFeatures();
|
||||
break;
|
||||
case 'notifications':
|
||||
markAllAsRead();
|
||||
break;
|
||||
case 'connection':
|
||||
acknowledgeIssue();
|
||||
break;
|
||||
case 'debug':
|
||||
acknowledgeAllIssues();
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear loading state after a delay
|
||||
setTimeout(() => setLoadingTab(null), 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[100]">
|
||||
<RadixDialog.Overlay asChild>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
|
||||
<RadixDialog.Content
|
||||
aria-describedby={undefined}
|
||||
onEscapeKeyDown={onClose}
|
||||
onPointerDownOutside={onClose}
|
||||
className="relative z-[101]"
|
||||
>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
'w-[1200px] h-[90vh]',
|
||||
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
||||
'rounded-2xl shadow-2xl',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'flex flex-col overflow-hidden',
|
||||
)}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
{activeTab || showTabManagement ? (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
||||
>
|
||||
<div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||
</button>
|
||||
) : (
|
||||
<motion.div
|
||||
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
|
||||
initial={{ rotate: -10 }}
|
||||
animate={{ rotate: 10 }}
|
||||
transition={{
|
||||
repeat: Infinity,
|
||||
repeatType: 'reverse',
|
||||
duration: 2,
|
||||
ease: 'easeInOut',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Only show Manage Tabs button in developer mode */}
|
||||
{!activeTab && !showTabManagement && developerMode && (
|
||||
<motion.button
|
||||
onClick={() => setShowTabManagement(true)}
|
||||
className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
|
||||
Manage Tabs
|
||||
</span>
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="developer-mode"
|
||||
checked={developerMode}
|
||||
onCheckedChange={handleDeveloperModeChange}
|
||||
className={classNames(
|
||||
'relative inline-flex h-6 w-11 items-center rounded-full',
|
||||
'bg-gray-200 dark:bg-gray-700',
|
||||
'data-[state=checked]:bg-purple-500',
|
||||
'transition-colors duration-200',
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Toggle developer mode</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-block h-4 w-4 transform rounded-full bg-white',
|
||||
'transition duration-200',
|
||||
'translate-x-1 data-[state=checked]:translate-x-6',
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<label
|
||||
htmlFor="developer-mode"
|
||||
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer"
|
||||
>
|
||||
{developerMode ? 'Developer Mode' : 'User Mode'}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
||||
>
|
||||
<div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-1',
|
||||
'overflow-y-auto',
|
||||
'hover:overflow-y-auto',
|
||||
'scrollbar scrollbar-w-2',
|
||||
'scrollbar-track-transparent',
|
||||
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
||||
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
||||
'will-change-scroll',
|
||||
'touch-auto',
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
key={activeTab || 'home'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="p-6"
|
||||
>
|
||||
{showTabManagement ? (
|
||||
<TabManagement />
|
||||
) : activeTab ? (
|
||||
getTabComponent()
|
||||
) : (
|
||||
<motion.div className="grid grid-cols-4 gap-4">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleTabs.map((tab: TabWithDevType, index: number) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: -20 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
delay: index * 0.05,
|
||||
}}
|
||||
>
|
||||
<DraggableTabTile
|
||||
tab={tab}
|
||||
index={index}
|
||||
moveTab={moveTab}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
</div>
|
||||
</RadixDialog.Portal>
|
||||
</RadixDialog.Root>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import { motion } from 'framer-motion';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { TabManagement } from './TabManagement';
|
||||
@ -481,14 +481,9 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'flex flex-col overflow-hidden',
|
||||
)}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{
|
||||
opacity: developerMode ? 1 : 0,
|
||||
scale: developerMode ? 1 : 0.95,
|
||||
y: developerMode ? 0 : 20,
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
@ -592,28 +587,54 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
||||
'touch-auto',
|
||||
)}
|
||||
>
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
|
||||
<motion.div
|
||||
key={activeTab || 'home'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="p-6"
|
||||
>
|
||||
{showTabManagement ? (
|
||||
<TabManagement />
|
||||
) : activeTab ? (
|
||||
getTabComponent()
|
||||
) : (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
|
||||
<DraggableTabTile
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
index={index}
|
||||
moveTab={moveTab}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<motion.div
|
||||
className="grid grid-cols-4 gap-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: -20 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
delay: index * 0.05,
|
||||
}}
|
||||
>
|
||||
<DraggableTabTile
|
||||
tab={tab}
|
||||
index={index}
|
||||
moveTab={moveTab}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { tabConfigurationStore, updateTabConfiguration, resetTabConfiguration } from '~/lib/stores/settings';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { TAB_LABELS, type TabType, type TabVisibilityConfig } from '~/components/settings/settings.types';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
|
||||
import {
|
||||
TAB_LABELS,
|
||||
DEFAULT_TAB_CONFIG,
|
||||
type TabType,
|
||||
type TabVisibilityConfig,
|
||||
} from '~/components/settings/settings.types';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
// Define icons for each tab type
|
||||
@ -23,152 +30,88 @@ const TAB_ICONS: Record<TabType, string> = {
|
||||
'service-status': 'i-ph:heartbeat-fill',
|
||||
};
|
||||
|
||||
interface TabGroupProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
tabs: TabVisibilityConfig[];
|
||||
onVisibilityChange: (tabId: TabType, enabled: boolean) => void;
|
||||
targetWindow: 'user' | 'developer';
|
||||
standardTabs: TabType[];
|
||||
interface DraggableTabProps {
|
||||
tab: TabVisibilityConfig;
|
||||
index: number;
|
||||
moveTab: (dragIndex: number, hoverIndex: number) => void;
|
||||
onVisibilityChange: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => {
|
||||
// Split tabs into visible and hidden
|
||||
const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
const DraggableTab = ({ tab, index, moveTab, onVisibilityChange }: DraggableTabProps) => {
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: 'tab-management',
|
||||
item: { index, id: tab.id },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: 'tab-management',
|
||||
hover: (item: { index: number; id: string }, monitor) => {
|
||||
if (!monitor.isOver({ shallow: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.id === tab.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.index === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveTab(item.index, index);
|
||||
item.index = index;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver({ shallow: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-8 rounded-xl bg-white/5 p-6 dark:bg-gray-800/30">
|
||||
<div className="mb-6">
|
||||
<h3 className="flex items-center gap-2 text-lg font-medium text-gray-900 dark:text-white">
|
||||
<span className="i-ph:layout-fill h-5 w-5 text-purple-500" />
|
||||
{title}
|
||||
</h3>
|
||||
{description && <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">{description}</p>}
|
||||
<motion.div
|
||||
ref={(node) => drag(drop(node))}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
style={{
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
cursor: 'move',
|
||||
}}
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-between rounded-lg border px-4 py-3 transition-all',
|
||||
isOver
|
||||
? 'border-purple-500 bg-purple-50/50 dark:border-purple-500/50 dark:bg-purple-500/10'
|
||||
: 'border-gray-200 bg-white hover:border-purple-200 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-purple-500/30',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={classNames(TAB_ICONS[tab.id], 'h-5 w-5 text-purple-500 dark:text-purple-400')} />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{TAB_LABELS[tab.id]}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<motion.div layout className="space-y-2">
|
||||
{visibleTabs.map((tab) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="group relative flex items-center justify-between rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm transition-all hover:border-purple-200 hover:shadow-md dark:border-gray-700 dark:bg-gray-800 dark:hover:border-purple-500/30"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={classNames(
|
||||
TAB_ICONS[tab.id],
|
||||
'h-5 w-5 transition-colors',
|
||||
tab.id === 'profile'
|
||||
? 'text-purple-500 dark:text-purple-400'
|
||||
: 'text-gray-500 group-hover:text-purple-500 dark:text-gray-400 dark:group-hover:text-purple-400',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
'text-sm font-medium transition-colors',
|
||||
tab.id === 'profile'
|
||||
? 'text-gray-900 dark:text-white'
|
||||
: 'text-gray-700 group-hover:text-gray-900 dark:text-gray-300 dark:group-hover:text-white',
|
||||
)}
|
||||
>
|
||||
{TAB_LABELS[tab.id]}
|
||||
</span>
|
||||
{tab.id === 'profile' && targetWindow === 'user' && (
|
||||
<span className="rounded-full bg-purple-50 px-2 py-0.5 text-xs font-medium text-purple-600 dark:bg-purple-500/10 dark:text-purple-400">
|
||||
Standard
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{targetWindow === 'user' ? (
|
||||
<label className="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tab.visible}
|
||||
onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
|
||||
'after:absolute after:left-[2px] after:top-[2px]',
|
||||
'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
|
||||
'after:transition-all after:content-[""]',
|
||||
'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
|
||||
'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Always visible</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{hiddenTabs.length > 0 && (
|
||||
<motion.div layout className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
<span className="i-ph:eye-slash-fill h-4 w-4" />
|
||||
Hidden Tabs
|
||||
</div>
|
||||
{hiddenTabs.map((tab) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="group relative flex items-center justify-between rounded-lg border border-gray-200 bg-white/50 px-4 py-3 transition-all hover:border-purple-200 dark:border-gray-700 dark:bg-gray-800/50 dark:hover:border-purple-500/30"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={classNames(
|
||||
TAB_ICONS[tab.id],
|
||||
'h-5 w-5 transition-colors',
|
||||
'text-gray-400 group-hover:text-purple-500 dark:text-gray-500 dark:group-hover:text-purple-400',
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-500 transition-colors group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white">
|
||||
{TAB_LABELS[tab.id]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{targetWindow === 'user' && (
|
||||
<label className="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tab.visible}
|
||||
onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
|
||||
'after:absolute after:left-[2px] after:top-[2px]',
|
||||
'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
|
||||
'after:transition-all after:content-[""]',
|
||||
'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
|
||||
'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="relative inline-flex cursor-pointer items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={tab.visible}
|
||||
onChange={(e) => onVisibilityChange(e.target.checked)}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
|
||||
'after:absolute after:left-[2px] after:top-[2px]',
|
||||
'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
|
||||
'after:transition-all after:content-[""]',
|
||||
'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
|
||||
'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -176,53 +119,64 @@ export const TabManagement = () => {
|
||||
const config = useStore(tabConfigurationStore);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Define standard (visible by default) tabs for each window
|
||||
const standardUserTabs: TabType[] = [
|
||||
'features',
|
||||
'data',
|
||||
'local-providers',
|
||||
'cloud-providers',
|
||||
'connection',
|
||||
'debug',
|
||||
'service-status',
|
||||
];
|
||||
const standardDeveloperTabs: TabType[] = [
|
||||
'profile',
|
||||
'settings',
|
||||
'notifications',
|
||||
'features',
|
||||
'data',
|
||||
'local-providers',
|
||||
'cloud-providers',
|
||||
'connection',
|
||||
'debug',
|
||||
'event-logs',
|
||||
'update',
|
||||
'task-manager',
|
||||
'service-status',
|
||||
];
|
||||
// Get ALL possible tabs for developer mode
|
||||
const allTabs = useMemo(() => {
|
||||
const uniqueTabs = new Set([
|
||||
...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
|
||||
...(config.userTabs || []).map((tab) => tab.id),
|
||||
...(config.developerTabs || []).map((tab) => tab.id),
|
||||
'event-logs', // Ensure these are always included
|
||||
'task-manager',
|
||||
]);
|
||||
|
||||
const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
|
||||
const tabs = targetWindow === 'user' ? config.userTabs : config.developerTabs;
|
||||
const existingTab = tabs.find((tab) => tab.id === tabId);
|
||||
return Array.from(uniqueTabs).map((tabId) => {
|
||||
const existingTab =
|
||||
config.developerTabs?.find((t) => t.id === tabId) ||
|
||||
config.userTabs?.find((t) => t.id === tabId) ||
|
||||
DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
|
||||
|
||||
const updatedTab: TabVisibilityConfig = existingTab
|
||||
? {
|
||||
...existingTab,
|
||||
visible: enabled,
|
||||
}
|
||||
: {
|
||||
id: tabId,
|
||||
visible: enabled,
|
||||
window: targetWindow,
|
||||
order: tabs.length,
|
||||
};
|
||||
return {
|
||||
id: tabId as TabType,
|
||||
visible: true,
|
||||
window: 'developer' as const,
|
||||
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
|
||||
};
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
// Update the store
|
||||
updateTabConfiguration(updatedTab);
|
||||
const handleVisibilityChange = (tabId: TabType, enabled: boolean) => {
|
||||
const updatedDevTabs = allTabs.map((tab) => {
|
||||
if (tab.id === tabId) {
|
||||
return { ...tab, visible: enabled };
|
||||
}
|
||||
|
||||
// Show toast notification
|
||||
toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'} in ${targetWindow} window`);
|
||||
return tab;
|
||||
});
|
||||
|
||||
tabConfigurationStore.set({
|
||||
...config,
|
||||
developerTabs: updatedDevTabs,
|
||||
});
|
||||
|
||||
toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'}`);
|
||||
};
|
||||
|
||||
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
||||
const newTabs = [...allTabs];
|
||||
const dragTab = newTabs[dragIndex];
|
||||
|
||||
newTabs.splice(dragIndex, 1);
|
||||
newTabs.splice(hoverIndex, 0, dragTab);
|
||||
|
||||
const updatedTabs = newTabs.map((tab, index) => ({
|
||||
...tab,
|
||||
order: index,
|
||||
}));
|
||||
|
||||
tabConfigurationStore.set({
|
||||
...config,
|
||||
developerTabs: updatedTabs,
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetToDefaults = () => {
|
||||
@ -230,38 +184,14 @@ export const TabManagement = () => {
|
||||
toast.success('Tab settings reset to defaults');
|
||||
};
|
||||
|
||||
// Filter tabs based on search and window
|
||||
const userTabs = (config.userTabs || []).filter(
|
||||
(tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
|
||||
);
|
||||
|
||||
const developerTabs = (config.developerTabs || []).filter(
|
||||
(tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
|
||||
);
|
||||
const filteredTabs = allTabs
|
||||
.filter((tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto px-6 py-6">
|
||||
<div className="mb-8">
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="flex items-center gap-2 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<span className="i-ph:squares-four-fill h-6 w-6 text-purple-500" />
|
||||
Tab Management
|
||||
</h2>
|
||||
<p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">
|
||||
Configure which tabs are visible in the user and developer windows
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleResetToDefaults}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-purple-50 px-4 py-2 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:bg-purple-500/10 dark:text-purple-400 dark:hover:bg-purple-500/20"
|
||||
>
|
||||
<span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
|
||||
@ -274,56 +204,31 @@ export const TabManagement = () => {
|
||||
className="block w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-10 pr-4 text-sm text-gray-900 placeholder:text-gray-500 focus:border-purple-500 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
{/* User Window Section */}
|
||||
<div className="rounded-xl border border-purple-100 bg-purple-50/50 p-1 dark:border-purple-500/10 dark:bg-purple-500/5">
|
||||
<div className="rounded-lg bg-white p-6 dark:bg-gray-800">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="rounded-lg bg-purple-100 p-2 dark:bg-purple-500/10">
|
||||
<span className="i-ph:user-circle-fill h-5 w-5 text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white">User Window</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to regular users</p>
|
||||
</div>
|
||||
</div>
|
||||
<TabGroup
|
||||
title="User Interface"
|
||||
description="Manage which tabs are visible in the user window"
|
||||
tabs={userTabs}
|
||||
onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'user')}
|
||||
targetWindow="user"
|
||||
standardTabs={standardUserTabs}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleResetToDefaults}
|
||||
className="ml-4 inline-flex items-center gap-1.5 rounded-lg bg-purple-50 px-4 py-2 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:bg-purple-500/10 dark:text-purple-400 dark:hover:bg-purple-500/20"
|
||||
>
|
||||
<span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Developer Window Section */}
|
||||
<div className="rounded-xl border border-blue-100 bg-blue-50/50 p-1 dark:border-blue-500/10 dark:bg-blue-500/5">
|
||||
<div className="rounded-lg bg-white p-6 dark:bg-gray-800">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-500/10">
|
||||
<span className="i-ph:code-fill h-5 w-5 text-blue-500 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 dark:text-white">Developer Window</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to developers</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-purple-100 bg-purple-50/50 p-6 dark:border-purple-500/10 dark:bg-purple-500/5">
|
||||
<AnimatePresence mode="popLayout">
|
||||
<div className="space-y-2">
|
||||
{filteredTabs.map((tab, index) => (
|
||||
<DraggableTab
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
index={index}
|
||||
moveTab={moveTab}
|
||||
onVisibilityChange={(enabled) => handleVisibilityChange(tab.id, enabled)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<TabGroup
|
||||
title="Developer Interface"
|
||||
description="Manage which tabs are visible in the developer window"
|
||||
tabs={developerTabs}
|
||||
onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'developer')}
|
||||
targetWindow="developer"
|
||||
standardTabs={standardDeveloperTabs}
|
||||
/>
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
@ -77,33 +77,29 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
||||
{ id: 'features', visible: true, window: 'user', order: 0 },
|
||||
{ id: 'data', visible: true, window: 'user', order: 1 },
|
||||
{ id: 'cloud-providers', visible: true, window: 'user', order: 2 },
|
||||
{ id: 'service-status', visible: true, window: 'user', order: 3 },
|
||||
{ id: 'local-providers', visible: true, window: 'user', order: 4 },
|
||||
{ id: 'connection', visible: true, window: 'user', order: 5 },
|
||||
{ id: 'debug', visible: true, window: 'user', order: 6 },
|
||||
{ id: 'local-providers', visible: true, window: 'user', order: 3 },
|
||||
{ id: 'connection', visible: true, window: 'user', order: 4 },
|
||||
{ id: 'debug', visible: true, window: 'user', order: 5 },
|
||||
|
||||
// User Window Tabs (Hidden by default)
|
||||
{ id: 'profile', visible: false, window: 'user', order: 7 },
|
||||
{ id: 'settings', visible: false, window: 'user', order: 8 },
|
||||
{ id: 'notifications', visible: false, window: 'user', order: 9 },
|
||||
{ id: 'event-logs', visible: false, window: 'user', order: 10 },
|
||||
{ id: 'update', visible: false, window: 'user', order: 11 },
|
||||
{ id: 'task-manager', visible: false, window: 'user', order: 12 },
|
||||
{ id: 'profile', visible: false, window: 'user', order: 6 },
|
||||
{ id: 'settings', visible: false, window: 'user', order: 7 },
|
||||
{ id: 'notifications', visible: false, window: 'user', order: 8 },
|
||||
{ id: 'event-logs', visible: false, window: 'user', order: 9 },
|
||||
{ id: 'update', visible: false, window: 'user', order: 10 },
|
||||
{ id: 'service-status', visible: false, window: 'user', order: 11 },
|
||||
|
||||
// Developer Window Tabs (All visible by default)
|
||||
{ id: 'profile', visible: true, window: 'developer', order: 0 },
|
||||
{ id: 'settings', visible: true, window: 'developer', order: 1 },
|
||||
{ id: 'notifications', visible: true, window: 'developer', order: 2 },
|
||||
{ id: 'features', visible: true, window: 'developer', order: 3 },
|
||||
{ id: 'data', visible: true, window: 'developer', order: 4 },
|
||||
{ id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
|
||||
{ id: 'local-providers', visible: true, window: 'developer', order: 6 },
|
||||
{ id: 'connection', visible: true, window: 'developer', order: 7 },
|
||||
{ id: 'debug', visible: true, window: 'developer', order: 8 },
|
||||
{ id: 'event-logs', visible: true, window: 'developer', order: 9 },
|
||||
{ id: 'update', visible: true, window: 'developer', order: 10 },
|
||||
{ id: 'task-manager', visible: true, window: 'developer', order: 11 },
|
||||
{ id: 'service-status', visible: true, window: 'developer', order: 12 },
|
||||
{ id: 'features', visible: true, window: 'developer', order: 0 },
|
||||
{ id: 'data', visible: true, window: 'developer', order: 1 },
|
||||
{ id: 'cloud-providers', visible: true, window: 'developer', order: 2 },
|
||||
{ id: 'local-providers', visible: true, window: 'developer', order: 3 },
|
||||
{ id: 'connection', visible: true, window: 'developer', order: 4 },
|
||||
{ id: 'debug', visible: true, window: 'developer', order: 5 },
|
||||
{ id: 'task-manager', visible: true, window: 'developer', order: 6 },
|
||||
{ id: 'settings', visible: true, window: 'developer', order: 7 },
|
||||
{ id: 'notifications', visible: true, window: 'developer', order: 8 },
|
||||
{ id: 'service-status', visible: true, window: 'developer', order: 9 },
|
||||
];
|
||||
|
||||
export const categoryLabels: Record<SettingCategory, string> = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { motion } from 'framer-motion';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { DialogTitle } from '~/components/ui/Dialog';
|
||||
@ -37,6 +37,7 @@ import {
|
||||
developerModeStore,
|
||||
setDeveloperMode,
|
||||
} from '~/lib/stores/settings';
|
||||
import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
|
||||
|
||||
interface DraggableTabTileProps {
|
||||
tab: TabVisibilityConfig;
|
||||
@ -123,6 +124,10 @@ interface UsersWindowProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface TabWithType extends TabVisibilityConfig {
|
||||
isExtraDevTab?: boolean;
|
||||
}
|
||||
|
||||
export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
||||
@ -223,45 +228,48 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
|
||||
// Only show tabs that are assigned to the user window AND are visible
|
||||
const visibleUserTabs = useMemo(() => {
|
||||
console.log('Filtering user tabs with configuration:', tabConfiguration);
|
||||
|
||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||
console.warn('Invalid tab configuration, using empty array');
|
||||
return [];
|
||||
}
|
||||
|
||||
return tabConfiguration.userTabs
|
||||
.filter((tab) => {
|
||||
if (!tab || typeof tab.id !== 'string') {
|
||||
console.warn('Invalid tab entry:', tab);
|
||||
return false;
|
||||
}
|
||||
// Get the base user tabs that are visible
|
||||
const baseTabs = tabConfiguration.userTabs.filter((tab) => {
|
||||
if (!tab || typeof tab.id !== 'string') {
|
||||
console.warn('Invalid tab entry:', tab);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
console.log('Hiding notifications tab due to disabled notifications');
|
||||
return false;
|
||||
}
|
||||
// Hide notifications tab if notifications are disabled
|
||||
if (tab.id === 'notifications' && !profile.notifications) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the tab has the required properties
|
||||
if (typeof tab.visible !== 'boolean' || typeof tab.window !== 'string' || typeof tab.order !== 'number') {
|
||||
console.warn('Tab missing required properties:', tab);
|
||||
return false;
|
||||
}
|
||||
// Only show tabs that are explicitly visible and assigned to the user window
|
||||
return tab.visible && tab.window === 'user';
|
||||
});
|
||||
|
||||
// Only show tabs that are explicitly visible and assigned to the user window
|
||||
const isVisible = tab.visible && tab.window === 'user';
|
||||
console.log(`Tab ${tab.id} visibility:`, isVisible);
|
||||
// If in developer mode, add the developer-only tabs
|
||||
if (developerMode) {
|
||||
const developerOnlyTabs = DEFAULT_TAB_CONFIG.filter((tab) => {
|
||||
/*
|
||||
* Only include tabs that are:
|
||||
* 1. Assigned to developer window
|
||||
* 2. Not already in user tabs
|
||||
* 3. Marked as visible in developer window
|
||||
*/
|
||||
return tab.window === 'developer' && tab.visible && !baseTabs.some((baseTab) => baseTab.id === tab.id);
|
||||
}).map((tab) => ({
|
||||
...tab,
|
||||
isExtraDevTab: true,
|
||||
order: baseTabs.length + tab.order, // Place after user tabs
|
||||
}));
|
||||
|
||||
return isVisible;
|
||||
})
|
||||
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => {
|
||||
const orderA = typeof a.order === 'number' ? a.order : 0;
|
||||
const orderB = typeof b.order === 'number' ? b.order : 0;
|
||||
return [...baseTabs, ...developerOnlyTabs].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
return orderA - orderB;
|
||||
});
|
||||
}, [tabConfiguration, profile.notifications]);
|
||||
return baseTabs.sort((a, b) => a.order - b.order);
|
||||
}, [tabConfiguration, profile.notifications, developerMode]);
|
||||
|
||||
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
||||
const draggedTab = visibleUserTabs[dragIndex];
|
||||
@ -569,29 +577,50 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
>
|
||||
<motion.div
|
||||
key={activeTab || 'home'}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="p-6"
|
||||
>
|
||||
{activeTab ? (
|
||||
getTabComponent()
|
||||
) : (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{visibleUserTabs.map((tab: TabVisibilityConfig, index: number) => (
|
||||
<DraggableTabTile
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
index={index}
|
||||
moveTab={moveTab}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<motion.div
|
||||
className="grid grid-cols-4 gap-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{visibleUserTabs.map((tab: TabWithType, index: number) => (
|
||||
<motion.div
|
||||
key={tab.id}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: -20 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
delay: index * 0.05,
|
||||
}}
|
||||
>
|
||||
<DraggableTabTile
|
||||
tab={tab}
|
||||
index={index}
|
||||
moveTab={moveTab}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
isActive={activeTab === tab.id}
|
||||
hasUpdate={getTabUpdateStatus(tab.id)}
|
||||
statusMessage={getStatusMessage(tab.id)}
|
||||
description={TAB_DESCRIPTIONS[tab.id]}
|
||||
isLoading={loadingTab === tab.id}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
@ -62,9 +62,11 @@ export const shortcutsStore = map<Shortcuts>({
|
||||
// Create a single key for provider settings
|
||||
const PROVIDER_SETTINGS_KEY = 'provider_settings';
|
||||
|
||||
// Add this helper function at the top of the file
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
// Initialize provider settings from both localStorage and defaults
|
||||
const getInitialProviderSettings = (): ProviderSetting => {
|
||||
const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
|
||||
const initialSettings: ProviderSetting = {};
|
||||
|
||||
// Start with default settings
|
||||
@ -77,17 +79,21 @@ const getInitialProviderSettings = (): ProviderSetting => {
|
||||
};
|
||||
});
|
||||
|
||||
// Override with saved settings if they exist
|
||||
if (savedSettings) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedSettings);
|
||||
Object.entries(parsed).forEach(([key, value]) => {
|
||||
if (initialSettings[key]) {
|
||||
initialSettings[key].settings = (value as IProviderConfig).settings;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing saved provider settings:', error);
|
||||
// Only try to load from localStorage in the browser
|
||||
if (isBrowser) {
|
||||
const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
|
||||
|
||||
if (savedSettings) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedSettings);
|
||||
Object.entries(parsed).forEach(([key, value]) => {
|
||||
if (initialSettings[key]) {
|
||||
initialSettings[key].settings = (value as IProviderConfig).settings;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing saved provider settings:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,11 +133,16 @@ const SETTINGS_KEYS = {
|
||||
EVENT_LOGS: 'isEventLogsEnabled',
|
||||
LOCAL_MODELS: 'isLocalModelsEnabled',
|
||||
PROMPT_ID: 'promptId',
|
||||
DEVELOPER_MODE: 'isDeveloperMode',
|
||||
} as const;
|
||||
|
||||
// Initialize settings from localStorage or defaults
|
||||
const getInitialSettings = () => {
|
||||
const getStoredBoolean = (key: string, defaultValue: boolean): boolean => {
|
||||
if (!isBrowser) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(key);
|
||||
|
||||
if (stored === null) {
|
||||
@ -151,7 +162,8 @@ const getInitialSettings = () => {
|
||||
contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false),
|
||||
eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
|
||||
localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true),
|
||||
promptId: localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default',
|
||||
promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default',
|
||||
developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false),
|
||||
};
|
||||
};
|
||||
|
||||
@ -196,65 +208,40 @@ export const updatePromptId = (id: string) => {
|
||||
localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
|
||||
};
|
||||
|
||||
// Initialize tab configuration from cookie or default
|
||||
const savedTabConfig = Cookies.get('tabConfiguration');
|
||||
console.log('Saved tab configuration:', savedTabConfig);
|
||||
|
||||
let initialTabConfig: TabWindowConfig;
|
||||
|
||||
try {
|
||||
if (savedTabConfig) {
|
||||
const parsedConfig = JSON.parse(savedTabConfig);
|
||||
|
||||
// Validate the parsed configuration
|
||||
if (
|
||||
parsedConfig &&
|
||||
Array.isArray(parsedConfig.userTabs) &&
|
||||
Array.isArray(parsedConfig.developerTabs) &&
|
||||
parsedConfig.userTabs.every(
|
||||
(tab: any) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
) &&
|
||||
parsedConfig.developerTabs.every(
|
||||
(tab: any) =>
|
||||
tab &&
|
||||
typeof tab.id === 'string' &&
|
||||
typeof tab.visible === 'boolean' &&
|
||||
typeof tab.window === 'string' &&
|
||||
typeof tab.order === 'number',
|
||||
)
|
||||
) {
|
||||
initialTabConfig = parsedConfig;
|
||||
console.log('Using saved tab configuration');
|
||||
} else {
|
||||
console.warn('Invalid saved tab configuration, using defaults');
|
||||
initialTabConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log('No saved tab configuration found, using defaults');
|
||||
initialTabConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading tab configuration:', error);
|
||||
initialTabConfig = {
|
||||
// Initialize tab configuration from localStorage or defaults
|
||||
const getInitialTabConfiguration = (): TabWindowConfig => {
|
||||
const defaultConfig: TabWindowConfig = {
|
||||
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
||||
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
||||
};
|
||||
}
|
||||
|
||||
console.log('Initial tab configuration:', initialTabConfig);
|
||||
if (!isBrowser) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
|
||||
try {
|
||||
const saved = localStorage.getItem('bolt_tab_configuration');
|
||||
|
||||
if (!saved) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(saved);
|
||||
|
||||
if (!parsed?.userTabs || !parsed?.developerTabs) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse tab configuration:', error);
|
||||
return defaultConfig;
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Initial tab configuration:', getInitialTabConfiguration());
|
||||
|
||||
export const tabConfigurationStore = map<TabWindowConfig>(getInitialTabConfiguration());
|
||||
|
||||
// Helper function to update tab configuration
|
||||
export const updateTabConfiguration = (config: TabVisibilityConfig) => {
|
||||
@ -307,9 +294,13 @@ export const resetTabConfiguration = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Developer mode store
|
||||
export const developerModeStore = atom<boolean>(false);
|
||||
// Developer mode store with persistence
|
||||
export const developerModeStore = atom<boolean>(initialSettings.developerMode);
|
||||
|
||||
export const setDeveloperMode = (value: boolean) => {
|
||||
developerModeStore.set(value);
|
||||
|
||||
if (isBrowser) {
|
||||
localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
@ -4,6 +4,8 @@ import { BaseChat } from '~/components/chat/BaseChat';
|
||||
import { Chat } from '~/components/chat/Chat.client';
|
||||
import { Header } from '~/components/header/Header';
|
||||
import BackgroundRays from '~/components/ui/BackgroundRays';
|
||||
import { ControlPanel } from '~/components/settings/ControlPanel';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
|
||||
@ -12,11 +14,23 @@ export const meta: MetaFunction = () => {
|
||||
export const loader = () => json({});
|
||||
|
||||
export default function Index() {
|
||||
const [showControlPanel, setShowControlPanel] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
|
||||
<BackgroundRays />
|
||||
<Header />
|
||||
<ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
|
||||
<button
|
||||
onClick={() => setShowControlPanel(true)}
|
||||
className="fixed bottom-4 right-4 flex items-center space-x-2 px-4 py-2 rounded-lg bg-purple-500 text-white hover:bg-purple-600 transition-colors"
|
||||
>
|
||||
<span className="i-ph:gear w-5 h-5" />
|
||||
<span>Open Control Panel</span>
|
||||
</button>
|
||||
<ClientOnly>
|
||||
{() => <ControlPanel open={showControlPanel} onClose={() => setShowControlPanel(false)} />}
|
||||
</ClientOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -78,6 +78,7 @@
|
||||
"@remix-run/cloudflare-pages": "^2.15.2",
|
||||
"@remix-run/node": "^2.15.2",
|
||||
"@remix-run/react": "^2.15.2",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.6",
|
||||
"@unocss/reset": "^0.61.9",
|
||||
"@webcontainer/api": "1.3.0-internal.10",
|
||||
@ -106,6 +107,7 @@
|
||||
"ollama-ai-provider": "^0.15.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
|
132
pnpm-lock.yaml
generated
132
pnpm-lock.yaml
generated
@ -152,6 +152,9 @@ importers:
|
||||
'@remix-run/react':
|
||||
specifier: ^2.15.2
|
||||
version: 2.15.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)
|
||||
'@types/react-beautiful-dnd':
|
||||
specifier: ^13.1.8
|
||||
version: 13.1.8
|
||||
'@uiw/codemirror-theme-vscode':
|
||||
specifier: ^4.23.6
|
||||
version: 4.23.7(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
|
||||
@ -236,12 +239,15 @@ importers:
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
react-beautiful-dnd:
|
||||
specifier: ^13.1.1
|
||||
version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-chartjs-2:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0(chart.js@4.4.7)(react@18.3.1)
|
||||
react-dnd:
|
||||
specifier: ^16.0.1
|
||||
version: 16.0.1(@types/node@22.10.10)(@types/react@18.3.18)(react@18.3.1)
|
||||
version: 16.0.1(@types/hoist-non-react-statics@3.3.6)(@types/node@22.10.10)(@types/react@18.3.18)(react@18.3.1)
|
||||
react-dnd-html5-backend:
|
||||
specifier: ^16.0.1
|
||||
version: 16.0.1
|
||||
@ -2802,6 +2808,9 @@ packages:
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.6':
|
||||
resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==}
|
||||
|
||||
'@types/js-cookie@3.0.6':
|
||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||
|
||||
@ -2829,11 +2838,17 @@ packages:
|
||||
'@types/prop-types@15.7.14':
|
||||
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
||||
|
||||
'@types/react-beautiful-dnd@13.1.8':
|
||||
resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
|
||||
|
||||
'@types/react-dom@18.3.5':
|
||||
resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.0.0
|
||||
|
||||
'@types/react-redux@7.1.34':
|
||||
resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==}
|
||||
|
||||
'@types/react@18.3.18':
|
||||
resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
|
||||
|
||||
@ -3482,6 +3497,9 @@ packages:
|
||||
resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
css-box-model@1.2.1:
|
||||
resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
|
||||
|
||||
css-tree@2.3.1:
|
||||
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
@ -4623,6 +4641,9 @@ packages:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
memoize-one@5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
|
||||
merge-descriptors@1.0.3:
|
||||
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
||||
|
||||
@ -5029,6 +5050,10 @@ packages:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-inspect@1.13.3:
|
||||
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -5330,6 +5355,9 @@ packages:
|
||||
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
|
||||
@ -5371,6 +5399,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
raf-schd@4.0.3:
|
||||
resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
|
||||
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
@ -5385,6 +5416,13 @@ packages:
|
||||
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
react-beautiful-dnd@13.1.1:
|
||||
resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
|
||||
deprecated: 'react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672'
|
||||
peerDependencies:
|
||||
react: ^16.8.5 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
|
||||
|
||||
react-chartjs-2@5.3.0:
|
||||
resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
|
||||
peerDependencies:
|
||||
@ -5428,12 +5466,27 @@ packages:
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-markdown@9.0.3:
|
||||
resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react-redux@7.2.9:
|
||||
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.3 || ^17 || ^18
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-refresh@0.14.2:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6090,6 +6143,9 @@ packages:
|
||||
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@ -6306,6 +6362,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-memo-one@1.1.3:
|
||||
resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
use-sidecar@1.1.3:
|
||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||
engines: {node: '>=10'}
|
||||
@ -9306,6 +9367,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.6':
|
||||
dependencies:
|
||||
'@types/react': 18.3.18
|
||||
hoist-non-react-statics: 3.3.2
|
||||
|
||||
'@types/js-cookie@3.0.6': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
@ -9330,10 +9396,21 @@ snapshots:
|
||||
|
||||
'@types/prop-types@15.7.14': {}
|
||||
|
||||
'@types/react-beautiful-dnd@13.1.8':
|
||||
dependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
'@types/react-dom@18.3.5(@types/react@18.3.18)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
'@types/react-redux@7.1.34':
|
||||
dependencies:
|
||||
'@types/hoist-non-react-statics': 3.3.6
|
||||
'@types/react': 18.3.18
|
||||
hoist-non-react-statics: 3.3.2
|
||||
redux: 4.2.1
|
||||
|
||||
'@types/react@18.3.18':
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.14
|
||||
@ -10171,6 +10248,10 @@ snapshots:
|
||||
randombytes: 2.1.0
|
||||
randomfill: 1.0.4
|
||||
|
||||
css-box-model@1.2.1:
|
||||
dependencies:
|
||||
tiny-invariant: 1.3.3
|
||||
|
||||
css-tree@2.3.1:
|
||||
dependencies:
|
||||
mdn-data: 2.0.30
|
||||
@ -11633,6 +11714,8 @@ snapshots:
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
memoize-one@5.2.1: {}
|
||||
|
||||
merge-descriptors@1.0.3: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
@ -12275,6 +12358,8 @@ snapshots:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.3: {}
|
||||
|
||||
object-is@1.1.6:
|
||||
@ -12566,6 +12651,12 @@ snapshots:
|
||||
err-code: 2.0.3
|
||||
retry: 0.12.0
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
@ -12614,6 +12705,8 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
raf-schd@4.0.3: {}
|
||||
|
||||
randombytes@2.1.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
@ -12632,6 +12725,20 @@ snapshots:
|
||||
iconv-lite: 0.4.24
|
||||
unpipe: 1.0.0
|
||||
|
||||
react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.7
|
||||
css-box-model: 1.2.1
|
||||
memoize-one: 5.2.1
|
||||
raf-schd: 4.0.3
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
redux: 4.2.1
|
||||
use-memo-one: 1.1.3(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- react-native
|
||||
|
||||
react-chartjs-2@5.3.0(chart.js@4.4.7)(react@18.3.1):
|
||||
dependencies:
|
||||
chart.js: 4.4.7
|
||||
@ -12641,7 +12748,7 @@ snapshots:
|
||||
dependencies:
|
||||
dnd-core: 16.0.1
|
||||
|
||||
react-dnd@16.0.1(@types/node@22.10.10)(@types/react@18.3.18)(react@18.3.1):
|
||||
react-dnd@16.0.1(@types/hoist-non-react-statics@3.3.6)(@types/node@22.10.10)(@types/react@18.3.18)(react@18.3.1):
|
||||
dependencies:
|
||||
'@react-dnd/invariant': 4.0.2
|
||||
'@react-dnd/shallowequal': 4.0.2
|
||||
@ -12650,6 +12757,7 @@ snapshots:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/hoist-non-react-statics': 3.3.6
|
||||
'@types/node': 22.10.10
|
||||
'@types/react': 18.3.18
|
||||
|
||||
@ -12670,6 +12778,8 @@ snapshots:
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-markdown@9.0.3(@types/react@18.3.18)(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@ -12687,6 +12797,18 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.7
|
||||
'@types/react-redux': 7.1.34
|
||||
hoist-non-react-statics: 3.3.2
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.3.1
|
||||
react-is: 17.0.2
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@18.3.1):
|
||||
@ -13414,6 +13536,8 @@ snapshots:
|
||||
dependencies:
|
||||
setimmediate: 1.0.5
|
||||
|
||||
tiny-invariant@1.3.3: {}
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@ -13656,6 +13780,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
use-memo-one@1.1.3(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
use-sidecar@1.1.3(@types/react@18.3.18)(react@18.3.1):
|
||||
dependencies:
|
||||
detect-node-es: 1.1.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user