Stijnus commited on
Commit
999d87b
·
1 Parent(s): af620d0

beta New control panel

Browse files

# 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

app/components/settings/ControlPanel.tsx ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { useStore } from '@nanostores/react';
4
+ import { Switch } from '@radix-ui/react-switch';
5
+ import * as RadixDialog from '@radix-ui/react-dialog';
6
+ import { DndProvider } from 'react-dnd';
7
+ import { HTML5Backend } from 'react-dnd-html5-backend';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { TabManagement } from './developer/TabManagement';
10
+ import { TabTile } from './shared/TabTile';
11
+ import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
12
+ import { useFeatures } from '~/lib/hooks/useFeatures';
13
+ import { useNotifications } from '~/lib/hooks/useNotifications';
14
+ import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
15
+ import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
16
+ import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
17
+ import type { TabType, TabVisibilityConfig } from './settings.types';
18
+ import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './settings.types';
19
+ import { resetTabConfiguration } from '~/lib/stores/settings';
20
+ import { DialogTitle } from '~/components/ui/Dialog';
21
+ import { useDrag, useDrop } from 'react-dnd';
22
+
23
+ // Import all tab components
24
+ import ProfileTab from './profile/ProfileTab';
25
+ import SettingsTab from './settings/SettingsTab';
26
+ import NotificationsTab from './notifications/NotificationsTab';
27
+ import FeaturesTab from './features/FeaturesTab';
28
+ import DataTab from './data/DataTab';
29
+ import DebugTab from './debug/DebugTab';
30
+ import { EventLogsTab } from './event-logs/EventLogsTab';
31
+ import UpdateTab from './update/UpdateTab';
32
+ import ConnectionsTab from './connections/ConnectionsTab';
33
+ import CloudProvidersTab from './providers/CloudProvidersTab';
34
+ import ServiceStatusTab from './providers/ServiceStatusTab';
35
+ import LocalProvidersTab from './providers/LocalProvidersTab';
36
+ import TaskManagerTab from './task-manager/TaskManagerTab';
37
+
38
+ interface ControlPanelProps {
39
+ open: boolean;
40
+ onClose: () => void;
41
+ }
42
+
43
+ interface TabWithDevType extends TabVisibilityConfig {
44
+ isExtraDevTab?: boolean;
45
+ }
46
+
47
+ const TAB_DESCRIPTIONS: Record<TabType, string> = {
48
+ profile: 'Manage your profile and account settings',
49
+ settings: 'Configure application preferences',
50
+ notifications: 'View and manage your notifications',
51
+ features: 'Explore new and upcoming features',
52
+ data: 'Manage your data and storage',
53
+ 'cloud-providers': 'Configure cloud AI providers and models',
54
+ 'local-providers': 'Configure local AI providers and models',
55
+ 'service-status': 'Monitor cloud LLM service status',
56
+ connection: 'Check connection status and settings',
57
+ debug: 'Debug tools and system information',
58
+ 'event-logs': 'View system events and logs',
59
+ update: 'Check for updates and release notes',
60
+ 'task-manager': 'Monitor system resources and processes',
61
+ };
62
+
63
+ // Add DraggableTabTile component before the ControlPanel component
64
+ const DraggableTabTile = ({
65
+ tab,
66
+ index,
67
+ moveTab,
68
+ ...props
69
+ }: {
70
+ tab: TabWithDevType;
71
+ index: number;
72
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
73
+ onClick: () => void;
74
+ isActive: boolean;
75
+ hasUpdate: boolean;
76
+ statusMessage: string;
77
+ description: string;
78
+ isLoading?: boolean;
79
+ }) => {
80
+ const [{ isDragging }, drag] = useDrag({
81
+ type: 'tab',
82
+ item: { index, id: tab.id },
83
+ collect: (monitor) => ({
84
+ isDragging: monitor.isDragging(),
85
+ }),
86
+ });
87
+
88
+ const [{ isOver, canDrop }, drop] = useDrop({
89
+ accept: 'tab',
90
+ hover: (item: { index: number; id: string }, monitor) => {
91
+ if (!monitor.isOver({ shallow: true })) {
92
+ return;
93
+ }
94
+
95
+ if (item.id === tab.id) {
96
+ return;
97
+ }
98
+
99
+ if (item.index === index) {
100
+ return;
101
+ }
102
+
103
+ // Only move when hovering over the middle section
104
+ const hoverBoundingRect = monitor.getSourceClientOffset();
105
+ const clientOffset = monitor.getClientOffset();
106
+
107
+ if (!hoverBoundingRect || !clientOffset) {
108
+ return;
109
+ }
110
+
111
+ const hoverMiddleX = hoverBoundingRect.x + 150; // Half of typical card width
112
+ const hoverClientX = clientOffset.x;
113
+
114
+ // Only perform the move when the mouse has crossed half of the items width
115
+ if (item.index < index && hoverClientX < hoverMiddleX) {
116
+ return;
117
+ }
118
+
119
+ if (item.index > index && hoverClientX > hoverMiddleX) {
120
+ return;
121
+ }
122
+
123
+ moveTab(item.index, index);
124
+ item.index = index;
125
+ },
126
+ collect: (monitor) => ({
127
+ isOver: monitor.isOver({ shallow: true }),
128
+ canDrop: monitor.canDrop(),
129
+ }),
130
+ });
131
+
132
+ const dropIndicatorClasses = classNames('rounded-xl border-2 border-transparent transition-all duration-200', {
133
+ 'ring-2 ring-purple-500 ring-opacity-50 bg-purple-50 dark:bg-purple-900/20': isOver,
134
+ 'hover:ring-2 hover:ring-purple-500/30': canDrop && !isOver,
135
+ });
136
+
137
+ return (
138
+ <motion.div
139
+ ref={(node) => drag(drop(node))}
140
+ style={{
141
+ opacity: isDragging ? 0.5 : 1,
142
+ cursor: 'move',
143
+ position: 'relative',
144
+ zIndex: isDragging ? 100 : isOver ? 50 : 1,
145
+ }}
146
+ animate={{
147
+ scale: isDragging ? 1.02 : isOver ? 1.05 : 1,
148
+ boxShadow: isDragging
149
+ ? '0 8px 24px rgba(0, 0, 0, 0.15)'
150
+ : isOver
151
+ ? '0 4px 12px rgba(147, 51, 234, 0.3)'
152
+ : '0 0 0 rgba(0, 0, 0, 0)',
153
+ borderColor: isOver ? 'rgb(147, 51, 234)' : isDragging ? 'rgba(147, 51, 234, 0.5)' : 'transparent',
154
+ y: isOver ? -2 : 0,
155
+ }}
156
+ transition={{
157
+ type: 'spring',
158
+ stiffness: 500,
159
+ damping: 30,
160
+ mass: 0.8,
161
+ }}
162
+ className={dropIndicatorClasses}
163
+ >
164
+ <TabTile {...props} tab={tab} />
165
+ {isOver && (
166
+ <motion.div
167
+ className="absolute inset-0 rounded-xl pointer-events-none"
168
+ initial={{ opacity: 0 }}
169
+ animate={{ opacity: 1 }}
170
+ exit={{ opacity: 0 }}
171
+ transition={{ duration: 0.2 }}
172
+ >
173
+ <div className="absolute inset-0 bg-gradient-to-r from-purple-500/10 to-purple-500/20 rounded-xl" />
174
+ <div className="absolute inset-0 border-2 border-purple-500/50 rounded-xl" />
175
+ </motion.div>
176
+ )}
177
+ </motion.div>
178
+ );
179
+ };
180
+
181
+ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
182
+ // State
183
+ const [activeTab, setActiveTab] = useState<TabType | null>(null);
184
+ const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
185
+ const [showTabManagement, setShowTabManagement] = useState(false);
186
+ const [profile, setProfile] = useState({ avatar: null, notifications: true });
187
+
188
+ // Store values
189
+ const tabConfiguration = useStore(tabConfigurationStore);
190
+ const developerMode = useStore(developerModeStore);
191
+
192
+ // Status hooks
193
+ const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
194
+ const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
195
+ const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
196
+ const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
197
+ const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
198
+
199
+ // Initialize profile from localStorage on mount
200
+ useEffect(() => {
201
+ if (typeof window === 'undefined') {
202
+ return;
203
+ }
204
+
205
+ const saved = localStorage.getItem('bolt_user_profile');
206
+
207
+ if (saved) {
208
+ try {
209
+ const parsedProfile = JSON.parse(saved);
210
+ setProfile(parsedProfile);
211
+ } catch (error) {
212
+ console.warn('Failed to parse profile from localStorage:', error);
213
+ }
214
+ }
215
+ }, []);
216
+
217
+ // Add visibleTabs logic using useMemo
218
+ const visibleTabs = useMemo(() => {
219
+ if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
220
+ console.warn('Invalid tab configuration, resetting to defaults');
221
+ resetTabConfiguration();
222
+
223
+ return [];
224
+ }
225
+
226
+ // In developer mode, show ALL tabs without restrictions
227
+ if (developerMode) {
228
+ // Combine all unique tabs from both user and developer configurations
229
+ const allTabs = new Set([
230
+ ...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
231
+ ...tabConfiguration.userTabs.map((tab) => tab.id),
232
+ ...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
233
+ ]);
234
+
235
+ // Create a complete tab list with all tabs visible
236
+ const devTabs = Array.from(allTabs).map((tabId) => {
237
+ // Try to find existing configuration for this tab
238
+ const existingTab =
239
+ tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
240
+ tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
241
+ DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
242
+
243
+ return {
244
+ id: tabId,
245
+ visible: true,
246
+ window: 'developer' as const,
247
+ order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
248
+ };
249
+ });
250
+
251
+ return devTabs.sort((a, b) => a.order - b.order);
252
+ }
253
+
254
+ // In user mode, only show visible user tabs
255
+ return tabConfiguration.userTabs
256
+ .filter((tab) => {
257
+ if (!tab || typeof tab.id !== 'string') {
258
+ console.warn('Invalid tab entry:', tab);
259
+ return false;
260
+ }
261
+
262
+ // Hide notifications tab if notifications are disabled
263
+ if (tab.id === 'notifications' && !profile.notifications) {
264
+ return false;
265
+ }
266
+
267
+ // Only show tabs that are explicitly visible and assigned to the user window
268
+ return tab.visible && tab.window === 'user';
269
+ })
270
+ .sort((a, b) => a.order - b.order);
271
+ }, [tabConfiguration, profile.notifications, developerMode]);
272
+
273
+ // Add moveTab handler
274
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
275
+ const newTabs = [...visibleTabs];
276
+ const dragTab = newTabs[dragIndex];
277
+ newTabs.splice(dragIndex, 1);
278
+ newTabs.splice(hoverIndex, 0, dragTab);
279
+
280
+ // Update the order of the tabs
281
+ const updatedTabs = newTabs.map((tab, index) => ({
282
+ ...tab,
283
+ order: index,
284
+ window: 'developer' as const,
285
+ visible: true,
286
+ }));
287
+
288
+ // Update the tab configuration store directly
289
+ if (developerMode) {
290
+ // In developer mode, update developerTabs while preserving configuration
291
+ tabConfigurationStore.set({
292
+ ...tabConfiguration,
293
+ developerTabs: updatedTabs,
294
+ });
295
+ } else {
296
+ // In user mode, update userTabs
297
+ tabConfigurationStore.set({
298
+ ...tabConfiguration,
299
+ userTabs: updatedTabs.map((tab) => ({ ...tab, window: 'user' as const })),
300
+ });
301
+ }
302
+ };
303
+
304
+ // Handlers
305
+ const handleBack = () => {
306
+ if (showTabManagement) {
307
+ setShowTabManagement(false);
308
+ } else if (activeTab) {
309
+ setActiveTab(null);
310
+ }
311
+ };
312
+
313
+ const handleDeveloperModeChange = (checked: boolean) => {
314
+ console.log('Developer mode changed:', checked);
315
+ setDeveloperMode(checked);
316
+ };
317
+
318
+ // Add effect to log developer mode changes
319
+ useEffect(() => {
320
+ console.log('Current developer mode:', developerMode);
321
+ }, [developerMode]);
322
+
323
+ const getTabComponent = () => {
324
+ switch (activeTab) {
325
+ case 'profile':
326
+ return <ProfileTab />;
327
+ case 'settings':
328
+ return <SettingsTab />;
329
+ case 'notifications':
330
+ return <NotificationsTab />;
331
+ case 'features':
332
+ return <FeaturesTab />;
333
+ case 'data':
334
+ return <DataTab />;
335
+ case 'cloud-providers':
336
+ return <CloudProvidersTab />;
337
+ case 'local-providers':
338
+ return <LocalProvidersTab />;
339
+ case 'connection':
340
+ return <ConnectionsTab />;
341
+ case 'debug':
342
+ return <DebugTab />;
343
+ case 'event-logs':
344
+ return <EventLogsTab />;
345
+ case 'update':
346
+ return <UpdateTab />;
347
+ case 'task-manager':
348
+ return <TaskManagerTab />;
349
+ case 'service-status':
350
+ return <ServiceStatusTab />;
351
+ default:
352
+ return null;
353
+ }
354
+ };
355
+
356
+ const getTabUpdateStatus = (tabId: TabType): boolean => {
357
+ switch (tabId) {
358
+ case 'update':
359
+ return hasUpdate;
360
+ case 'features':
361
+ return hasNewFeatures;
362
+ case 'notifications':
363
+ return hasUnreadNotifications;
364
+ case 'connection':
365
+ return hasConnectionIssues;
366
+ case 'debug':
367
+ return hasActiveWarnings;
368
+ default:
369
+ return false;
370
+ }
371
+ };
372
+
373
+ const getStatusMessage = (tabId: TabType): string => {
374
+ switch (tabId) {
375
+ case 'update':
376
+ return `New update available (v${currentVersion})`;
377
+ case 'features':
378
+ return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
379
+ case 'notifications':
380
+ return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
381
+ case 'connection':
382
+ return currentIssue === 'disconnected'
383
+ ? 'Connection lost'
384
+ : currentIssue === 'high-latency'
385
+ ? 'High latency detected'
386
+ : 'Connection issues detected';
387
+ case 'debug': {
388
+ const warnings = activeIssues.filter((i) => i.type === 'warning').length;
389
+ const errors = activeIssues.filter((i) => i.type === 'error').length;
390
+
391
+ return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
392
+ }
393
+ default:
394
+ return '';
395
+ }
396
+ };
397
+
398
+ const handleTabClick = (tabId: TabType) => {
399
+ setLoadingTab(tabId);
400
+ setActiveTab(tabId);
401
+
402
+ // Acknowledge notifications based on tab
403
+ switch (tabId) {
404
+ case 'update':
405
+ acknowledgeUpdate();
406
+ break;
407
+ case 'features':
408
+ acknowledgeAllFeatures();
409
+ break;
410
+ case 'notifications':
411
+ markAllAsRead();
412
+ break;
413
+ case 'connection':
414
+ acknowledgeIssue();
415
+ break;
416
+ case 'debug':
417
+ acknowledgeAllIssues();
418
+ break;
419
+ }
420
+
421
+ // Clear loading state after a delay
422
+ setTimeout(() => setLoadingTab(null), 500);
423
+ };
424
+
425
+ return (
426
+ <DndProvider backend={HTML5Backend}>
427
+ <RadixDialog.Root open={open}>
428
+ <RadixDialog.Portal>
429
+ <div className="fixed inset-0 flex items-center justify-center z-[100]">
430
+ <RadixDialog.Overlay asChild>
431
+ <motion.div
432
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
433
+ initial={{ opacity: 0 }}
434
+ animate={{ opacity: 1 }}
435
+ exit={{ opacity: 0 }}
436
+ transition={{ duration: 0.2 }}
437
+ />
438
+ </RadixDialog.Overlay>
439
+
440
+ <RadixDialog.Content
441
+ aria-describedby={undefined}
442
+ onEscapeKeyDown={onClose}
443
+ onPointerDownOutside={onClose}
444
+ className="relative z-[101]"
445
+ >
446
+ <motion.div
447
+ className={classNames(
448
+ 'w-[1200px] h-[90vh]',
449
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
450
+ 'rounded-2xl shadow-2xl',
451
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
452
+ 'flex flex-col overflow-hidden',
453
+ )}
454
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
455
+ animate={{ opacity: 1, scale: 1, y: 0 }}
456
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
457
+ transition={{ duration: 0.2 }}
458
+ >
459
+ {/* Header */}
460
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
461
+ <div className="flex items-center space-x-4">
462
+ {activeTab || showTabManagement ? (
463
+ <button
464
+ onClick={handleBack}
465
+ 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"
466
+ >
467
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
468
+ </button>
469
+ ) : (
470
+ <motion.div
471
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
472
+ initial={{ rotate: -10 }}
473
+ animate={{ rotate: 10 }}
474
+ transition={{
475
+ repeat: Infinity,
476
+ repeatType: 'reverse',
477
+ duration: 2,
478
+ ease: 'easeInOut',
479
+ }}
480
+ />
481
+ )}
482
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
483
+ {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
484
+ </DialogTitle>
485
+ </div>
486
+
487
+ <div className="flex items-center space-x-4">
488
+ {/* Only show Manage Tabs button in developer mode */}
489
+ {!activeTab && !showTabManagement && developerMode && (
490
+ <motion.button
491
+ onClick={() => setShowTabManagement(true)}
492
+ 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"
493
+ whileHover={{ scale: 1.05 }}
494
+ whileTap={{ scale: 0.95 }}
495
+ >
496
+ <div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
497
+ <span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
498
+ Manage Tabs
499
+ </span>
500
+ </motion.button>
501
+ )}
502
+
503
+ <div className="flex items-center gap-2">
504
+ <Switch
505
+ id="developer-mode"
506
+ checked={developerMode}
507
+ onCheckedChange={handleDeveloperModeChange}
508
+ className={classNames(
509
+ 'relative inline-flex h-6 w-11 items-center rounded-full',
510
+ 'bg-gray-200 dark:bg-gray-700',
511
+ 'data-[state=checked]:bg-purple-500',
512
+ 'transition-colors duration-200',
513
+ )}
514
+ >
515
+ <span className="sr-only">Toggle developer mode</span>
516
+ <span
517
+ className={classNames(
518
+ 'inline-block h-4 w-4 transform rounded-full bg-white',
519
+ 'transition duration-200',
520
+ 'translate-x-1 data-[state=checked]:translate-x-6',
521
+ )}
522
+ />
523
+ </Switch>
524
+ <label
525
+ htmlFor="developer-mode"
526
+ className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer"
527
+ >
528
+ {developerMode ? 'Developer Mode' : 'User Mode'}
529
+ </label>
530
+ </div>
531
+
532
+ <button
533
+ onClick={onClose}
534
+ 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"
535
+ >
536
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
537
+ </button>
538
+ </div>
539
+ </div>
540
+
541
+ {/* Content */}
542
+ <div
543
+ className={classNames(
544
+ 'flex-1',
545
+ 'overflow-y-auto',
546
+ 'hover:overflow-y-auto',
547
+ 'scrollbar scrollbar-w-2',
548
+ 'scrollbar-track-transparent',
549
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
550
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
551
+ 'will-change-scroll',
552
+ 'touch-auto',
553
+ )}
554
+ >
555
+ <motion.div
556
+ key={activeTab || 'home'}
557
+ initial={{ opacity: 0 }}
558
+ animate={{ opacity: 1 }}
559
+ exit={{ opacity: 0 }}
560
+ transition={{ duration: 0.2 }}
561
+ className="p-6"
562
+ >
563
+ {showTabManagement ? (
564
+ <TabManagement />
565
+ ) : activeTab ? (
566
+ getTabComponent()
567
+ ) : (
568
+ <motion.div className="grid grid-cols-4 gap-4">
569
+ <AnimatePresence mode="popLayout">
570
+ {visibleTabs.map((tab: TabWithDevType, index: number) => (
571
+ <motion.div
572
+ key={tab.id}
573
+ layout
574
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
575
+ animate={{ opacity: 1, scale: 1, y: 0 }}
576
+ exit={{ opacity: 0, scale: 0.8, y: -20 }}
577
+ transition={{
578
+ duration: 0.2,
579
+ delay: index * 0.05,
580
+ }}
581
+ >
582
+ <DraggableTabTile
583
+ tab={tab}
584
+ index={index}
585
+ moveTab={moveTab}
586
+ onClick={() => handleTabClick(tab.id)}
587
+ isActive={activeTab === tab.id}
588
+ hasUpdate={getTabUpdateStatus(tab.id)}
589
+ statusMessage={getStatusMessage(tab.id)}
590
+ description={TAB_DESCRIPTIONS[tab.id]}
591
+ isLoading={loadingTab === tab.id}
592
+ />
593
+ </motion.div>
594
+ ))}
595
+ </AnimatePresence>
596
+ </motion.div>
597
+ )}
598
+ </motion.div>
599
+ </div>
600
+ </motion.div>
601
+ </RadixDialog.Content>
602
+ </div>
603
+ </RadixDialog.Portal>
604
+ </RadixDialog.Root>
605
+ </DndProvider>
606
+ );
607
+ };
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
- import { motion } from 'framer-motion';
3
  import { useState, useEffect, useMemo } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
  import { TabManagement } from './TabManagement';
@@ -481,14 +481,9 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
481
  'border border-[#E5E5E5] dark:border-[#1A1A1A]',
482
  'flex flex-col overflow-hidden',
483
  )}
484
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
485
- animate={{
486
- opacity: developerMode ? 1 : 0,
487
- scale: developerMode ? 1 : 0.95,
488
- y: developerMode ? 0 : 20,
489
- }}
490
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
491
- transition={{ duration: 0.2 }}
492
  >
493
  {/* Header */}
494
  <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) => {
592
  'touch-auto',
593
  )}
594
  >
595
- <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
 
 
 
 
 
 
 
596
  {showTabManagement ? (
597
  <TabManagement />
598
  ) : activeTab ? (
599
  getTabComponent()
600
  ) : (
601
- <div className="grid grid-cols-4 gap-4">
602
- {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
603
- <DraggableTabTile
604
- key={tab.id}
605
- tab={tab}
606
- index={index}
607
- moveTab={moveTab}
608
- onClick={() => handleTabClick(tab.id)}
609
- isActive={activeTab === tab.id}
610
- hasUpdate={getTabUpdateStatus(tab.id)}
611
- statusMessage={getStatusMessage(tab.id)}
612
- description={TAB_DESCRIPTIONS[tab.id]}
613
- isLoading={loadingTab === tab.id}
614
- />
615
- ))}
616
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  )}
618
  </motion.div>
619
  </div>
 
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
  import { useState, useEffect, useMemo } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
  import { TabManagement } from './TabManagement';
 
481
  'border border-[#E5E5E5] dark:border-[#1A1A1A]',
482
  'flex flex-col overflow-hidden',
483
  )}
484
+ initial={{ opacity: 1 }}
485
+ animate={{ opacity: 1 }}
486
+ transition={{ duration: 0.15 }}
 
 
 
 
 
487
  >
488
  {/* Header */}
489
  <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
 
587
  'touch-auto',
588
  )}
589
  >
590
+ <motion.div
591
+ key={activeTab || 'home'}
592
+ initial={{ opacity: 0 }}
593
+ animate={{ opacity: 1 }}
594
+ exit={{ opacity: 0 }}
595
+ transition={{ duration: 0.2 }}
596
+ className="p-6"
597
+ >
598
  {showTabManagement ? (
599
  <TabManagement />
600
  ) : activeTab ? (
601
  getTabComponent()
602
  ) : (
603
+ <motion.div
604
+ className="grid grid-cols-4 gap-4"
605
+ initial={{ opacity: 0 }}
606
+ animate={{ opacity: 1 }}
607
+ exit={{ opacity: 0 }}
608
+ transition={{ duration: 0.2 }}
609
+ >
610
+ <AnimatePresence mode="popLayout">
611
+ {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
612
+ <motion.div
613
+ key={tab.id}
614
+ layout
615
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
616
+ animate={{ opacity: 1, scale: 1, y: 0 }}
617
+ exit={{ opacity: 0, scale: 0.8, y: -20 }}
618
+ transition={{
619
+ duration: 0.2,
620
+ delay: index * 0.05,
621
+ }}
622
+ >
623
+ <DraggableTabTile
624
+ tab={tab}
625
+ index={index}
626
+ moveTab={moveTab}
627
+ onClick={() => handleTabClick(tab.id)}
628
+ isActive={activeTab === tab.id}
629
+ hasUpdate={getTabUpdateStatus(tab.id)}
630
+ statusMessage={getStatusMessage(tab.id)}
631
+ description={TAB_DESCRIPTIONS[tab.id]}
632
+ isLoading={loadingTab === tab.id}
633
+ />
634
+ </motion.div>
635
+ ))}
636
+ </AnimatePresence>
637
+ </motion.div>
638
  )}
639
  </motion.div>
640
  </div>
app/components/settings/developer/TabManagement.tsx CHANGED
@@ -1,9 +1,16 @@
1
- import { motion } from 'framer-motion';
2
- import { useState } from 'react';
3
- import { classNames } from '~/utils/classNames';
4
- import { tabConfigurationStore, updateTabConfiguration, resetTabConfiguration } from '~/lib/stores/settings';
5
  import { useStore } from '@nanostores/react';
6
- import { TAB_LABELS, type TabType, type TabVisibilityConfig } from '~/components/settings/settings.types';
 
 
 
 
 
 
 
 
 
7
  import { toast } from 'react-toastify';
8
 
9
  // Define icons for each tab type
@@ -23,152 +30,88 @@ const TAB_ICONS: Record<TabType, string> = {
23
  'service-status': 'i-ph:heartbeat-fill',
24
  };
25
 
26
- interface TabGroupProps {
27
- title: string;
28
- description?: string;
29
- tabs: TabVisibilityConfig[];
30
- onVisibilityChange: (tabId: TabType, enabled: boolean) => void;
31
- targetWindow: 'user' | 'developer';
32
- standardTabs: TabType[];
33
  }
34
 
35
- const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => {
36
- // Split tabs into visible and hidden
37
- const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
38
- const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  return (
41
- <div className="mb-8 rounded-xl bg-white/5 p-6 dark:bg-gray-800/30">
42
- <div className="mb-6">
43
- <h3 className="flex items-center gap-2 text-lg font-medium text-gray-900 dark:text-white">
44
- <span className="i-ph:layout-fill h-5 w-5 text-purple-500" />
45
- {title}
46
- </h3>
47
- {description && <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">{description}</p>}
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  </div>
49
-
50
- <div className="space-y-6">
51
- <motion.div layout className="space-y-2">
52
- {visibleTabs.map((tab) => (
53
- <motion.div
54
- key={tab.id}
55
- layout
56
- initial={{ opacity: 0, y: 20 }}
57
- animate={{ opacity: 1, y: 0 }}
58
- exit={{ opacity: 0, y: -20 }}
59
- transition={{ duration: 0.2 }}
60
- 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"
61
- >
62
- <div className="flex items-center space-x-3">
63
- <div
64
- className={classNames(
65
- TAB_ICONS[tab.id],
66
- 'h-5 w-5 transition-colors',
67
- tab.id === 'profile'
68
- ? 'text-purple-500 dark:text-purple-400'
69
- : 'text-gray-500 group-hover:text-purple-500 dark:text-gray-400 dark:group-hover:text-purple-400',
70
- )}
71
- />
72
- <span
73
- className={classNames(
74
- 'text-sm font-medium transition-colors',
75
- tab.id === 'profile'
76
- ? 'text-gray-900 dark:text-white'
77
- : 'text-gray-700 group-hover:text-gray-900 dark:text-gray-300 dark:group-hover:text-white',
78
- )}
79
- >
80
- {TAB_LABELS[tab.id]}
81
- </span>
82
- {tab.id === 'profile' && targetWindow === 'user' && (
83
- <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">
84
- Standard
85
- </span>
86
- )}
87
- </div>
88
- <div className="flex items-center space-x-4">
89
- {targetWindow === 'user' ? (
90
- <label className="relative inline-flex cursor-pointer items-center">
91
- <input
92
- type="checkbox"
93
- checked={tab.visible}
94
- onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
95
- className="peer sr-only"
96
- />
97
- <div
98
- className={classNames(
99
- 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
100
- 'after:absolute after:left-[2px] after:top-[2px]',
101
- 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
102
- 'after:transition-all after:content-[""]',
103
- 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
104
- 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
105
- )}
106
- />
107
- </label>
108
- ) : (
109
- <div className="text-sm text-gray-500 dark:text-gray-400">Always visible</div>
110
- )}
111
- </div>
112
- </motion.div>
113
- ))}
114
- </motion.div>
115
-
116
- {hiddenTabs.length > 0 && (
117
- <motion.div layout className="space-y-2">
118
- <div className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
119
- <span className="i-ph:eye-slash-fill h-4 w-4" />
120
- Hidden Tabs
121
- </div>
122
- {hiddenTabs.map((tab) => (
123
- <motion.div
124
- key={tab.id}
125
- layout
126
- initial={{ opacity: 0, y: 20 }}
127
- animate={{ opacity: 1, y: 0 }}
128
- exit={{ opacity: 0, y: -20 }}
129
- transition={{ duration: 0.2 }}
130
- 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"
131
- >
132
- <div className="flex items-center space-x-3">
133
- <div
134
- className={classNames(
135
- TAB_ICONS[tab.id],
136
- 'h-5 w-5 transition-colors',
137
- 'text-gray-400 group-hover:text-purple-500 dark:text-gray-500 dark:group-hover:text-purple-400',
138
- )}
139
- />
140
- <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">
141
- {TAB_LABELS[tab.id]}
142
- </span>
143
- </div>
144
- <div className="flex items-center space-x-4">
145
- {targetWindow === 'user' && (
146
- <label className="relative inline-flex cursor-pointer items-center">
147
- <input
148
- type="checkbox"
149
- checked={tab.visible}
150
- onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
151
- className="peer sr-only"
152
- />
153
- <div
154
- className={classNames(
155
- 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
156
- 'after:absolute after:left-[2px] after:top-[2px]',
157
- 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
158
- 'after:transition-all after:content-[""]',
159
- 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
160
- 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
161
- )}
162
- />
163
- </label>
164
- )}
165
- </div>
166
- </motion.div>
167
- ))}
168
- </motion.div>
169
- )}
170
  </div>
171
- </div>
172
  );
173
  };
174
 
@@ -176,53 +119,64 @@ export const TabManagement = () => {
176
  const config = useStore(tabConfigurationStore);
177
  const [searchQuery, setSearchQuery] = useState('');
178
 
179
- // Define standard (visible by default) tabs for each window
180
- const standardUserTabs: TabType[] = [
181
- 'features',
182
- 'data',
183
- 'local-providers',
184
- 'cloud-providers',
185
- 'connection',
186
- 'debug',
187
- 'service-status',
188
- ];
189
- const standardDeveloperTabs: TabType[] = [
190
- 'profile',
191
- 'settings',
192
- 'notifications',
193
- 'features',
194
- 'data',
195
- 'local-providers',
196
- 'cloud-providers',
197
- 'connection',
198
- 'debug',
199
- 'event-logs',
200
- 'update',
201
- 'task-manager',
202
- 'service-status',
203
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
- const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
206
- const tabs = targetWindow === 'user' ? config.userTabs : config.developerTabs;
207
- const existingTab = tabs.find((tab) => tab.id === tabId);
208
 
209
- const updatedTab: TabVisibilityConfig = existingTab
210
- ? {
211
- ...existingTab,
212
- visible: enabled,
213
- }
214
- : {
215
- id: tabId,
216
- visible: enabled,
217
- window: targetWindow,
218
- order: tabs.length,
219
- };
220
 
221
- // Update the store
222
- updateTabConfiguration(updatedTab);
 
 
223
 
224
- // Show toast notification
225
- toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'} in ${targetWindow} window`);
 
 
226
  };
227
 
228
  const handleResetToDefaults = () => {
@@ -230,38 +184,14 @@ export const TabManagement = () => {
230
  toast.success('Tab settings reset to defaults');
231
  };
232
 
233
- // Filter tabs based on search and window
234
- const userTabs = (config.userTabs || []).filter(
235
- (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
236
- );
237
-
238
- const developerTabs = (config.developerTabs || []).filter(
239
- (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
240
- );
241
 
242
  return (
243
- <div className="h-full overflow-y-auto px-6 py-6">
244
- <div className="mb-8">
245
  <div className="flex items-center justify-between">
246
- <div>
247
- <h2 className="flex items-center gap-2 text-xl font-semibold text-gray-900 dark:text-white">
248
- <span className="i-ph:squares-four-fill h-6 w-6 text-purple-500" />
249
- Tab Management
250
- </h2>
251
- <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">
252
- Configure which tabs are visible in the user and developer windows
253
- </p>
254
- </div>
255
- <button
256
- onClick={handleResetToDefaults}
257
- 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"
258
- >
259
- <span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
260
- Reset to Defaults
261
- </button>
262
- </div>
263
-
264
- <div className="mt-6 flex items-center gap-4">
265
  <div className="relative flex-1">
266
  <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
267
  <span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
@@ -274,56 +204,31 @@ export const TabManagement = () => {
274
  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"
275
  />
276
  </div>
277
- </div>
278
- </div>
279
-
280
- <div className="space-y-8">
281
- {/* User Window Section */}
282
- <div className="rounded-xl border border-purple-100 bg-purple-50/50 p-1 dark:border-purple-500/10 dark:bg-purple-500/5">
283
- <div className="rounded-lg bg-white p-6 dark:bg-gray-800">
284
- <div className="mb-6 flex items-center gap-3">
285
- <div className="rounded-lg bg-purple-100 p-2 dark:bg-purple-500/10">
286
- <span className="i-ph:user-circle-fill h-5 w-5 text-purple-500 dark:text-purple-400" />
287
- </div>
288
- <div>
289
- <h3 className="text-base font-medium text-gray-900 dark:text-white">User Window</h3>
290
- <p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to regular users</p>
291
- </div>
292
- </div>
293
- <TabGroup
294
- title="User Interface"
295
- description="Manage which tabs are visible in the user window"
296
- tabs={userTabs}
297
- onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'user')}
298
- targetWindow="user"
299
- standardTabs={standardUserTabs}
300
- />
301
- </div>
302
  </div>
303
 
304
- {/* Developer Window Section */}
305
- <div className="rounded-xl border border-blue-100 bg-blue-50/50 p-1 dark:border-blue-500/10 dark:bg-blue-500/5">
306
- <div className="rounded-lg bg-white p-6 dark:bg-gray-800">
307
- <div className="mb-6 flex items-center gap-3">
308
- <div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-500/10">
309
- <span className="i-ph:code-fill h-5 w-5 text-blue-500 dark:text-blue-400" />
310
- </div>
311
- <div>
312
- <h3 className="text-base font-medium text-gray-900 dark:text-white">Developer Window</h3>
313
- <p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to developers</p>
314
- </div>
 
315
  </div>
316
- <TabGroup
317
- title="Developer Interface"
318
- description="Manage which tabs are visible in the developer window"
319
- tabs={developerTabs}
320
- onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'developer')}
321
- targetWindow="developer"
322
- standardTabs={standardDeveloperTabs}
323
- />
324
- </div>
325
  </div>
326
  </div>
327
- </div>
328
  );
329
  };
 
1
+ import { motion, AnimatePresence } from 'framer-motion';
2
+ import { useState, useMemo } from 'react';
 
 
3
  import { useStore } from '@nanostores/react';
4
+ import { DndProvider, useDrag, useDrop } from 'react-dnd';
5
+ import { HTML5Backend } from 'react-dnd-html5-backend';
6
+ import { classNames } from '~/utils/classNames';
7
+ import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
8
+ import {
9
+ TAB_LABELS,
10
+ DEFAULT_TAB_CONFIG,
11
+ type TabType,
12
+ type TabVisibilityConfig,
13
+ } from '~/components/settings/settings.types';
14
  import { toast } from 'react-toastify';
15
 
16
  // Define icons for each tab type
 
30
  'service-status': 'i-ph:heartbeat-fill',
31
  };
32
 
33
+ interface DraggableTabProps {
34
+ tab: TabVisibilityConfig;
35
+ index: number;
36
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
37
+ onVisibilityChange: (enabled: boolean) => void;
 
 
38
  }
39
 
40
+ const DraggableTab = ({ tab, index, moveTab, onVisibilityChange }: DraggableTabProps) => {
41
+ const [{ isDragging }, drag] = useDrag({
42
+ type: 'tab-management',
43
+ item: { index, id: tab.id },
44
+ collect: (monitor) => ({
45
+ isDragging: monitor.isDragging(),
46
+ }),
47
+ });
48
+
49
+ const [{ isOver }, drop] = useDrop({
50
+ accept: 'tab-management',
51
+ hover: (item: { index: number; id: string }, monitor) => {
52
+ if (!monitor.isOver({ shallow: true })) {
53
+ return;
54
+ }
55
+
56
+ if (item.id === tab.id) {
57
+ return;
58
+ }
59
+
60
+ if (item.index === index) {
61
+ return;
62
+ }
63
+
64
+ moveTab(item.index, index);
65
+ item.index = index;
66
+ },
67
+ collect: (monitor) => ({
68
+ isOver: monitor.isOver({ shallow: true }),
69
+ }),
70
+ });
71
 
72
  return (
73
+ <motion.div
74
+ ref={(node) => drag(drop(node))}
75
+ layout
76
+ initial={{ opacity: 0, y: 20 }}
77
+ animate={{ opacity: 1, y: 0 }}
78
+ exit={{ opacity: 0, y: -20 }}
79
+ style={{
80
+ opacity: isDragging ? 0.5 : 1,
81
+ cursor: 'move',
82
+ }}
83
+ className={classNames(
84
+ 'group relative flex items-center justify-between rounded-lg border px-4 py-3 transition-all',
85
+ isOver
86
+ ? 'border-purple-500 bg-purple-50/50 dark:border-purple-500/50 dark:bg-purple-500/10'
87
+ : 'border-gray-200 bg-white hover:border-purple-200 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-purple-500/30',
88
+ )}
89
+ >
90
+ <div className="flex items-center space-x-3">
91
+ <div className={classNames(TAB_ICONS[tab.id], 'h-5 w-5 text-purple-500 dark:text-purple-400')} />
92
+ <span className="text-sm font-medium text-gray-900 dark:text-white">{TAB_LABELS[tab.id]}</span>
93
  </div>
94
+ <div className="flex items-center space-x-4">
95
+ <label className="relative inline-flex cursor-pointer items-center">
96
+ <input
97
+ type="checkbox"
98
+ checked={tab.visible}
99
+ onChange={(e) => onVisibilityChange(e.target.checked)}
100
+ className="peer sr-only"
101
+ />
102
+ <div
103
+ className={classNames(
104
+ 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
105
+ 'after:absolute after:left-[2px] after:top-[2px]',
106
+ 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
107
+ 'after:transition-all after:content-[""]',
108
+ 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
109
+ 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
110
+ )}
111
+ />
112
+ </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  </div>
114
+ </motion.div>
115
  );
116
  };
117
 
 
119
  const config = useStore(tabConfigurationStore);
120
  const [searchQuery, setSearchQuery] = useState('');
121
 
122
+ // Get ALL possible tabs for developer mode
123
+ const allTabs = useMemo(() => {
124
+ const uniqueTabs = new Set([
125
+ ...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
126
+ ...(config.userTabs || []).map((tab) => tab.id),
127
+ ...(config.developerTabs || []).map((tab) => tab.id),
128
+ 'event-logs', // Ensure these are always included
129
+ 'task-manager',
130
+ ]);
131
+
132
+ return Array.from(uniqueTabs).map((tabId) => {
133
+ const existingTab =
134
+ config.developerTabs?.find((t) => t.id === tabId) ||
135
+ config.userTabs?.find((t) => t.id === tabId) ||
136
+ DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
137
+
138
+ return {
139
+ id: tabId as TabType,
140
+ visible: true,
141
+ window: 'developer' as const,
142
+ order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
143
+ };
144
+ });
145
+ }, [config]);
146
+
147
+ const handleVisibilityChange = (tabId: TabType, enabled: boolean) => {
148
+ const updatedDevTabs = allTabs.map((tab) => {
149
+ if (tab.id === tabId) {
150
+ return { ...tab, visible: enabled };
151
+ }
152
+
153
+ return tab;
154
+ });
155
+
156
+ tabConfigurationStore.set({
157
+ ...config,
158
+ developerTabs: updatedDevTabs,
159
+ });
160
+
161
+ toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'}`);
162
+ };
163
 
164
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
165
+ const newTabs = [...allTabs];
166
+ const dragTab = newTabs[dragIndex];
167
 
168
+ newTabs.splice(dragIndex, 1);
169
+ newTabs.splice(hoverIndex, 0, dragTab);
 
 
 
 
 
 
 
 
 
170
 
171
+ const updatedTabs = newTabs.map((tab, index) => ({
172
+ ...tab,
173
+ order: index,
174
+ }));
175
 
176
+ tabConfigurationStore.set({
177
+ ...config,
178
+ developerTabs: updatedTabs,
179
+ });
180
  };
181
 
182
  const handleResetToDefaults = () => {
 
184
  toast.success('Tab settings reset to defaults');
185
  };
186
 
187
+ const filteredTabs = allTabs
188
+ .filter((tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()))
189
+ .sort((a, b) => a.order - b.order);
 
 
 
 
 
190
 
191
  return (
192
+ <DndProvider backend={HTML5Backend}>
193
+ <div className="space-y-6">
194
  <div className="flex items-center justify-between">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  <div className="relative flex-1">
196
  <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
197
  <span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
 
204
  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"
205
  />
206
  </div>
207
+ <button
208
+ onClick={handleResetToDefaults}
209
+ 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"
210
+ >
211
+ <span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
212
+ Reset to Defaults
213
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </div>
215
 
216
+ <div className="rounded-xl border border-purple-100 bg-purple-50/50 p-6 dark:border-purple-500/10 dark:bg-purple-500/5">
217
+ <AnimatePresence mode="popLayout">
218
+ <div className="space-y-2">
219
+ {filteredTabs.map((tab, index) => (
220
+ <DraggableTab
221
+ key={tab.id}
222
+ tab={tab}
223
+ index={index}
224
+ moveTab={moveTab}
225
+ onVisibilityChange={(enabled) => handleVisibilityChange(tab.id, enabled)}
226
+ />
227
+ ))}
228
  </div>
229
+ </AnimatePresence>
 
 
 
 
 
 
 
 
230
  </div>
231
  </div>
232
+ </DndProvider>
233
  );
234
  };
app/components/settings/settings.types.ts CHANGED
@@ -77,33 +77,29 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
77
  { id: 'features', visible: true, window: 'user', order: 0 },
78
  { id: 'data', visible: true, window: 'user', order: 1 },
79
  { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
80
- { id: 'service-status', visible: true, window: 'user', order: 3 },
81
- { id: 'local-providers', visible: true, window: 'user', order: 4 },
82
- { id: 'connection', visible: true, window: 'user', order: 5 },
83
- { id: 'debug', visible: true, window: 'user', order: 6 },
84
 
85
  // User Window Tabs (Hidden by default)
86
- { id: 'profile', visible: false, window: 'user', order: 7 },
87
- { id: 'settings', visible: false, window: 'user', order: 8 },
88
- { id: 'notifications', visible: false, window: 'user', order: 9 },
89
- { id: 'event-logs', visible: false, window: 'user', order: 10 },
90
- { id: 'update', visible: false, window: 'user', order: 11 },
91
- { id: 'task-manager', visible: false, window: 'user', order: 12 },
92
 
93
  // Developer Window Tabs (All visible by default)
94
- { id: 'profile', visible: true, window: 'developer', order: 0 },
95
- { id: 'settings', visible: true, window: 'developer', order: 1 },
96
- { id: 'notifications', visible: true, window: 'developer', order: 2 },
97
- { id: 'features', visible: true, window: 'developer', order: 3 },
98
- { id: 'data', visible: true, window: 'developer', order: 4 },
99
- { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
100
- { id: 'local-providers', visible: true, window: 'developer', order: 6 },
101
- { id: 'connection', visible: true, window: 'developer', order: 7 },
102
- { id: 'debug', visible: true, window: 'developer', order: 8 },
103
- { id: 'event-logs', visible: true, window: 'developer', order: 9 },
104
- { id: 'update', visible: true, window: 'developer', order: 10 },
105
- { id: 'task-manager', visible: true, window: 'developer', order: 11 },
106
- { id: 'service-status', visible: true, window: 'developer', order: 12 },
107
  ];
108
 
109
  export const categoryLabels: Record<SettingCategory, string> = {
 
77
  { id: 'features', visible: true, window: 'user', order: 0 },
78
  { id: 'data', visible: true, window: 'user', order: 1 },
79
  { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
80
+ { id: 'local-providers', visible: true, window: 'user', order: 3 },
81
+ { id: 'connection', visible: true, window: 'user', order: 4 },
82
+ { id: 'debug', visible: true, window: 'user', order: 5 },
 
83
 
84
  // User Window Tabs (Hidden by default)
85
+ { id: 'profile', visible: false, window: 'user', order: 6 },
86
+ { id: 'settings', visible: false, window: 'user', order: 7 },
87
+ { id: 'notifications', visible: false, window: 'user', order: 8 },
88
+ { id: 'event-logs', visible: false, window: 'user', order: 9 },
89
+ { id: 'update', visible: false, window: 'user', order: 10 },
90
+ { id: 'service-status', visible: false, window: 'user', order: 11 },
91
 
92
  // Developer Window Tabs (All visible by default)
93
+ { id: 'features', visible: true, window: 'developer', order: 0 },
94
+ { id: 'data', visible: true, window: 'developer', order: 1 },
95
+ { id: 'cloud-providers', visible: true, window: 'developer', order: 2 },
96
+ { id: 'local-providers', visible: true, window: 'developer', order: 3 },
97
+ { id: 'connection', visible: true, window: 'developer', order: 4 },
98
+ { id: 'debug', visible: true, window: 'developer', order: 5 },
99
+ { id: 'task-manager', visible: true, window: 'developer', order: 6 },
100
+ { id: 'settings', visible: true, window: 'developer', order: 7 },
101
+ { id: 'notifications', visible: true, window: 'developer', order: 8 },
102
+ { id: 'service-status', visible: true, window: 'developer', order: 9 },
 
 
 
103
  ];
104
 
105
  export const categoryLabels: Record<SettingCategory, string> = {
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
- import { motion } from 'framer-motion';
4
  import React, { useState, useEffect, useMemo } from 'react';
5
  import { classNames } from '~/utils/classNames';
6
  import { DialogTitle } from '~/components/ui/Dialog';
@@ -37,6 +37,7 @@ import {
37
  developerModeStore,
38
  setDeveloperMode,
39
  } from '~/lib/stores/settings';
 
40
 
41
  interface DraggableTabTileProps {
42
  tab: TabVisibilityConfig;
@@ -123,6 +124,10 @@ interface UsersWindowProps {
123
  onClose: () => void;
124
  }
125
 
 
 
 
 
126
  export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
127
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
128
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
@@ -223,45 +228,48 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
223
 
224
  // Only show tabs that are assigned to the user window AND are visible
225
  const visibleUserTabs = useMemo(() => {
226
- console.log('Filtering user tabs with configuration:', tabConfiguration);
227
-
228
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
229
  console.warn('Invalid tab configuration, using empty array');
230
  return [];
231
  }
232
 
233
- return tabConfiguration.userTabs
234
- .filter((tab) => {
235
- if (!tab || typeof tab.id !== 'string') {
236
- console.warn('Invalid tab entry:', tab);
237
- return false;
238
- }
239
-
240
- // Hide notifications tab if notifications are disabled
241
- if (tab.id === 'notifications' && !profile.notifications) {
242
- console.log('Hiding notifications tab due to disabled notifications');
243
- return false;
244
- }
245
-
246
- // Ensure the tab has the required properties
247
- if (typeof tab.visible !== 'boolean' || typeof tab.window !== 'string' || typeof tab.order !== 'number') {
248
- console.warn('Tab missing required properties:', tab);
249
- return false;
250
- }
251
-
252
- // Only show tabs that are explicitly visible and assigned to the user window
253
- const isVisible = tab.visible && tab.window === 'user';
254
- console.log(`Tab ${tab.id} visibility:`, isVisible);
255
-
256
- return isVisible;
257
- })
258
- .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => {
259
- const orderA = typeof a.order === 'number' ? a.order : 0;
260
- const orderB = typeof b.order === 'number' ? b.order : 0;
261
-
262
- return orderA - orderB;
263
- });
264
- }, [tabConfiguration, profile.notifications]);
 
 
 
 
 
265
 
266
  const moveTab = (dragIndex: number, hoverIndex: number) => {
267
  const draggedTab = visibleUserTabs[dragIndex];
@@ -569,29 +577,50 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
569
  >
570
  <motion.div
571
  key={activeTab || 'home'}
572
- initial={{ opacity: 0, y: 20 }}
573
- animate={{ opacity: 1, y: 0 }}
 
 
574
  className="p-6"
575
  >
576
  {activeTab ? (
577
  getTabComponent()
578
  ) : (
579
- <div className="grid grid-cols-4 gap-4">
580
- {visibleUserTabs.map((tab: TabVisibilityConfig, index: number) => (
581
- <DraggableTabTile
582
- key={tab.id}
583
- tab={tab}
584
- index={index}
585
- moveTab={moveTab}
586
- onClick={() => handleTabClick(tab.id)}
587
- isActive={activeTab === tab.id}
588
- hasUpdate={getTabUpdateStatus(tab.id)}
589
- statusMessage={getStatusMessage(tab.id)}
590
- description={TAB_DESCRIPTIONS[tab.id]}
591
- isLoading={loadingTab === tab.id}
592
- />
593
- ))}
594
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  )}
596
  </motion.div>
597
  </div>
 
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
  import React, { useState, useEffect, useMemo } from 'react';
5
  import { classNames } from '~/utils/classNames';
6
  import { DialogTitle } from '~/components/ui/Dialog';
 
37
  developerModeStore,
38
  setDeveloperMode,
39
  } from '~/lib/stores/settings';
40
+ import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
41
 
42
  interface DraggableTabTileProps {
43
  tab: TabVisibilityConfig;
 
124
  onClose: () => void;
125
  }
126
 
127
+ interface TabWithType extends TabVisibilityConfig {
128
+ isExtraDevTab?: boolean;
129
+ }
130
+
131
  export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
132
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
133
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
 
228
 
229
  // Only show tabs that are assigned to the user window AND are visible
230
  const visibleUserTabs = useMemo(() => {
 
 
231
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
232
  console.warn('Invalid tab configuration, using empty array');
233
  return [];
234
  }
235
 
236
+ // Get the base user tabs that are visible
237
+ const baseTabs = tabConfiguration.userTabs.filter((tab) => {
238
+ if (!tab || typeof tab.id !== 'string') {
239
+ console.warn('Invalid tab entry:', tab);
240
+ return false;
241
+ }
242
+
243
+ // Hide notifications tab if notifications are disabled
244
+ if (tab.id === 'notifications' && !profile.notifications) {
245
+ return false;
246
+ }
247
+
248
+ // Only show tabs that are explicitly visible and assigned to the user window
249
+ return tab.visible && tab.window === 'user';
250
+ });
251
+
252
+ // If in developer mode, add the developer-only tabs
253
+ if (developerMode) {
254
+ const developerOnlyTabs = DEFAULT_TAB_CONFIG.filter((tab) => {
255
+ /*
256
+ * Only include tabs that are:
257
+ * 1. Assigned to developer window
258
+ * 2. Not already in user tabs
259
+ * 3. Marked as visible in developer window
260
+ */
261
+ return tab.window === 'developer' && tab.visible && !baseTabs.some((baseTab) => baseTab.id === tab.id);
262
+ }).map((tab) => ({
263
+ ...tab,
264
+ isExtraDevTab: true,
265
+ order: baseTabs.length + tab.order, // Place after user tabs
266
+ }));
267
+
268
+ return [...baseTabs, ...developerOnlyTabs].sort((a, b) => a.order - b.order);
269
+ }
270
+
271
+ return baseTabs.sort((a, b) => a.order - b.order);
272
+ }, [tabConfiguration, profile.notifications, developerMode]);
273
 
274
  const moveTab = (dragIndex: number, hoverIndex: number) => {
275
  const draggedTab = visibleUserTabs[dragIndex];
 
577
  >
578
  <motion.div
579
  key={activeTab || 'home'}
580
+ initial={{ opacity: 0 }}
581
+ animate={{ opacity: 1 }}
582
+ exit={{ opacity: 0 }}
583
+ transition={{ duration: 0.2 }}
584
  className="p-6"
585
  >
586
  {activeTab ? (
587
  getTabComponent()
588
  ) : (
589
+ <motion.div
590
+ className="grid grid-cols-4 gap-4"
591
+ initial={{ opacity: 0 }}
592
+ animate={{ opacity: 1 }}
593
+ exit={{ opacity: 0 }}
594
+ transition={{ duration: 0.2 }}
595
+ >
596
+ <AnimatePresence mode="popLayout">
597
+ {visibleUserTabs.map((tab: TabWithType, index: number) => (
598
+ <motion.div
599
+ key={tab.id}
600
+ layout
601
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
602
+ animate={{ opacity: 1, scale: 1, y: 0 }}
603
+ exit={{ opacity: 0, scale: 0.8, y: -20 }}
604
+ transition={{
605
+ duration: 0.2,
606
+ delay: index * 0.05,
607
+ }}
608
+ >
609
+ <DraggableTabTile
610
+ tab={tab}
611
+ index={index}
612
+ moveTab={moveTab}
613
+ onClick={() => handleTabClick(tab.id)}
614
+ isActive={activeTab === tab.id}
615
+ hasUpdate={getTabUpdateStatus(tab.id)}
616
+ statusMessage={getStatusMessage(tab.id)}
617
+ description={TAB_DESCRIPTIONS[tab.id]}
618
+ isLoading={loadingTab === tab.id}
619
+ />
620
+ </motion.div>
621
+ ))}
622
+ </AnimatePresence>
623
+ </motion.div>
624
  )}
625
  </motion.div>
626
  </div>
app/lib/stores/settings.ts CHANGED
@@ -62,9 +62,11 @@ export const shortcutsStore = map<Shortcuts>({
62
  // Create a single key for provider settings
63
  const PROVIDER_SETTINGS_KEY = 'provider_settings';
64
 
 
 
 
65
  // Initialize provider settings from both localStorage and defaults
66
  const getInitialProviderSettings = (): ProviderSetting => {
67
- const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
68
  const initialSettings: ProviderSetting = {};
69
 
70
  // Start with default settings
@@ -77,17 +79,21 @@ const getInitialProviderSettings = (): ProviderSetting => {
77
  };
78
  });
79
 
80
- // Override with saved settings if they exist
81
- if (savedSettings) {
82
- try {
83
- const parsed = JSON.parse(savedSettings);
84
- Object.entries(parsed).forEach(([key, value]) => {
85
- if (initialSettings[key]) {
86
- initialSettings[key].settings = (value as IProviderConfig).settings;
87
- }
88
- });
89
- } catch (error) {
90
- console.error('Error parsing saved provider settings:', error);
 
 
 
 
91
  }
92
  }
93
 
@@ -127,11 +133,16 @@ const SETTINGS_KEYS = {
127
  EVENT_LOGS: 'isEventLogsEnabled',
128
  LOCAL_MODELS: 'isLocalModelsEnabled',
129
  PROMPT_ID: 'promptId',
 
130
  } as const;
131
 
132
  // Initialize settings from localStorage or defaults
133
  const getInitialSettings = () => {
134
  const getStoredBoolean = (key: string, defaultValue: boolean): boolean => {
 
 
 
 
135
  const stored = localStorage.getItem(key);
136
 
137
  if (stored === null) {
@@ -151,7 +162,8 @@ const getInitialSettings = () => {
151
  contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false),
152
  eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
153
  localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true),
154
- promptId: localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default',
 
155
  };
156
  };
157
 
@@ -196,65 +208,40 @@ export const updatePromptId = (id: string) => {
196
  localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
197
  };
198
 
199
- // Initialize tab configuration from cookie or default
200
- const savedTabConfig = Cookies.get('tabConfiguration');
201
- console.log('Saved tab configuration:', savedTabConfig);
202
-
203
- let initialTabConfig: TabWindowConfig;
204
-
205
- try {
206
- if (savedTabConfig) {
207
- const parsedConfig = JSON.parse(savedTabConfig);
208
-
209
- // Validate the parsed configuration
210
- if (
211
- parsedConfig &&
212
- Array.isArray(parsedConfig.userTabs) &&
213
- Array.isArray(parsedConfig.developerTabs) &&
214
- parsedConfig.userTabs.every(
215
- (tab: any) =>
216
- tab &&
217
- typeof tab.id === 'string' &&
218
- typeof tab.visible === 'boolean' &&
219
- typeof tab.window === 'string' &&
220
- typeof tab.order === 'number',
221
- ) &&
222
- parsedConfig.developerTabs.every(
223
- (tab: any) =>
224
- tab &&
225
- typeof tab.id === 'string' &&
226
- typeof tab.visible === 'boolean' &&
227
- typeof tab.window === 'string' &&
228
- typeof tab.order === 'number',
229
- )
230
- ) {
231
- initialTabConfig = parsedConfig;
232
- console.log('Using saved tab configuration');
233
- } else {
234
- console.warn('Invalid saved tab configuration, using defaults');
235
- initialTabConfig = {
236
- userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
237
- developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
238
- };
239
- }
240
- } else {
241
- console.log('No saved tab configuration found, using defaults');
242
- initialTabConfig = {
243
- userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
244
- developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
245
- };
246
- }
247
- } catch (error) {
248
- console.error('Error loading tab configuration:', error);
249
- initialTabConfig = {
250
  userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
251
  developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
252
  };
253
- }
254
 
255
- console.log('Initial tab configuration:', initialTabConfig);
 
 
256
 
257
- export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  // Helper function to update tab configuration
260
  export const updateTabConfiguration = (config: TabVisibilityConfig) => {
@@ -307,9 +294,13 @@ export const resetTabConfiguration = () => {
307
  });
308
  };
309
 
310
- // Developer mode store
311
- export const developerModeStore = atom<boolean>(false);
312
 
313
  export const setDeveloperMode = (value: boolean) => {
314
  developerModeStore.set(value);
 
 
 
 
315
  };
 
62
  // Create a single key for provider settings
63
  const PROVIDER_SETTINGS_KEY = 'provider_settings';
64
 
65
+ // Add this helper function at the top of the file
66
+ const isBrowser = typeof window !== 'undefined';
67
+
68
  // Initialize provider settings from both localStorage and defaults
69
  const getInitialProviderSettings = (): ProviderSetting => {
 
70
  const initialSettings: ProviderSetting = {};
71
 
72
  // Start with default settings
 
79
  };
80
  });
81
 
82
+ // Only try to load from localStorage in the browser
83
+ if (isBrowser) {
84
+ const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
85
+
86
+ if (savedSettings) {
87
+ try {
88
+ const parsed = JSON.parse(savedSettings);
89
+ Object.entries(parsed).forEach(([key, value]) => {
90
+ if (initialSettings[key]) {
91
+ initialSettings[key].settings = (value as IProviderConfig).settings;
92
+ }
93
+ });
94
+ } catch (error) {
95
+ console.error('Error parsing saved provider settings:', error);
96
+ }
97
  }
98
  }
99
 
 
133
  EVENT_LOGS: 'isEventLogsEnabled',
134
  LOCAL_MODELS: 'isLocalModelsEnabled',
135
  PROMPT_ID: 'promptId',
136
+ DEVELOPER_MODE: 'isDeveloperMode',
137
  } as const;
138
 
139
  // Initialize settings from localStorage or defaults
140
  const getInitialSettings = () => {
141
  const getStoredBoolean = (key: string, defaultValue: boolean): boolean => {
142
+ if (!isBrowser) {
143
+ return defaultValue;
144
+ }
145
+
146
  const stored = localStorage.getItem(key);
147
 
148
  if (stored === null) {
 
162
  contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false),
163
  eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
164
  localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true),
165
+ promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default',
166
+ developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false),
167
  };
168
  };
169
 
 
208
  localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
209
  };
210
 
211
+ // Initialize tab configuration from localStorage or defaults
212
+ const getInitialTabConfiguration = (): TabWindowConfig => {
213
+ const defaultConfig: TabWindowConfig = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
215
  developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
216
  };
 
217
 
218
+ if (!isBrowser) {
219
+ return defaultConfig;
220
+ }
221
 
222
+ try {
223
+ const saved = localStorage.getItem('bolt_tab_configuration');
224
+
225
+ if (!saved) {
226
+ return defaultConfig;
227
+ }
228
+
229
+ const parsed = JSON.parse(saved);
230
+
231
+ if (!parsed?.userTabs || !parsed?.developerTabs) {
232
+ return defaultConfig;
233
+ }
234
+
235
+ return parsed;
236
+ } catch (error) {
237
+ console.warn('Failed to parse tab configuration:', error);
238
+ return defaultConfig;
239
+ }
240
+ };
241
+
242
+ console.log('Initial tab configuration:', getInitialTabConfiguration());
243
+
244
+ export const tabConfigurationStore = map<TabWindowConfig>(getInitialTabConfiguration());
245
 
246
  // Helper function to update tab configuration
247
  export const updateTabConfiguration = (config: TabVisibilityConfig) => {
 
294
  });
295
  };
296
 
297
+ // Developer mode store with persistence
298
+ export const developerModeStore = atom<boolean>(initialSettings.developerMode);
299
 
300
  export const setDeveloperMode = (value: boolean) => {
301
  developerModeStore.set(value);
302
+
303
+ if (isBrowser) {
304
+ localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
305
+ }
306
  };
app/routes/_index.tsx CHANGED
@@ -4,6 +4,8 @@ import { BaseChat } from '~/components/chat/BaseChat';
4
  import { Chat } from '~/components/chat/Chat.client';
5
  import { Header } from '~/components/header/Header';
6
  import BackgroundRays from '~/components/ui/BackgroundRays';
 
 
7
 
8
  export const meta: MetaFunction = () => {
9
  return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
@@ -12,11 +14,23 @@ export const meta: MetaFunction = () => {
12
  export const loader = () => json({});
13
 
14
  export default function Index() {
 
 
15
  return (
16
  <div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
17
  <BackgroundRays />
18
  <Header />
19
  <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
 
 
 
 
 
 
 
 
 
 
20
  </div>
21
  );
22
  }
 
4
  import { Chat } from '~/components/chat/Chat.client';
5
  import { Header } from '~/components/header/Header';
6
  import BackgroundRays from '~/components/ui/BackgroundRays';
7
+ import { ControlPanel } from '~/components/settings/ControlPanel';
8
+ import { useState } from 'react';
9
 
10
  export const meta: MetaFunction = () => {
11
  return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
 
14
  export const loader = () => json({});
15
 
16
  export default function Index() {
17
+ const [showControlPanel, setShowControlPanel] = useState(false);
18
+
19
  return (
20
  <div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
21
  <BackgroundRays />
22
  <Header />
23
  <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
24
+ <button
25
+ onClick={() => setShowControlPanel(true)}
26
+ 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"
27
+ >
28
+ <span className="i-ph:gear w-5 h-5" />
29
+ <span>Open Control Panel</span>
30
+ </button>
31
+ <ClientOnly>
32
+ {() => <ControlPanel open={showControlPanel} onClose={() => setShowControlPanel(false)} />}
33
+ </ClientOnly>
34
  </div>
35
  );
36
  }
package.json CHANGED
@@ -78,6 +78,7 @@
78
  "@remix-run/cloudflare-pages": "^2.15.2",
79
  "@remix-run/node": "^2.15.2",
80
  "@remix-run/react": "^2.15.2",
 
81
  "@uiw/codemirror-theme-vscode": "^4.23.6",
82
  "@unocss/reset": "^0.61.9",
83
  "@webcontainer/api": "1.3.0-internal.10",
@@ -106,6 +107,7 @@
106
  "ollama-ai-provider": "^0.15.2",
107
  "path-browserify": "^1.0.1",
108
  "react": "^18.3.1",
 
109
  "react-chartjs-2": "^5.3.0",
110
  "react-dnd": "^16.0.1",
111
  "react-dnd-html5-backend": "^16.0.1",
 
78
  "@remix-run/cloudflare-pages": "^2.15.2",
79
  "@remix-run/node": "^2.15.2",
80
  "@remix-run/react": "^2.15.2",
81
+ "@types/react-beautiful-dnd": "^13.1.8",
82
  "@uiw/codemirror-theme-vscode": "^4.23.6",
83
  "@unocss/reset": "^0.61.9",
84
  "@webcontainer/api": "1.3.0-internal.10",
 
107
  "ollama-ai-provider": "^0.15.2",
108
  "path-browserify": "^1.0.1",
109
  "react": "^18.3.1",
110
+ "react-beautiful-dnd": "^13.1.1",
111
  "react-chartjs-2": "^5.3.0",
112
  "react-dnd": "^16.0.1",
113
  "react-dnd-html5-backend": "^16.0.1",
pnpm-lock.yaml CHANGED
@@ -152,6 +152,9 @@ importers:
152
  '@remix-run/react':
153
  specifier: ^2.15.2
154
  version: 2.15.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)
 
 
 
155
  '@uiw/codemirror-theme-vscode':
156
  specifier: ^4.23.6
157
  version: 4.23.7(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
@@ -236,12 +239,15 @@ importers:
236
  react:
237
  specifier: ^18.3.1
238
  version: 18.3.1
 
 
 
239
  react-chartjs-2:
240
  specifier: ^5.3.0
241
  version: 5.3.0(chart.js@4.4.7)(react@18.3.1)
242
  react-dnd:
243
  specifier: ^16.0.1
244
- version: 16.0.1(@types/node@22.10.10)(@types/react@18.3.18)(react@18.3.1)
245
  react-dnd-html5-backend:
246
  specifier: ^16.0.1
247
  version: 16.0.1
@@ -2802,6 +2808,9 @@ packages:
2802
  '@types/hast@3.0.4':
2803
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
2804
 
 
 
 
2805
  '@types/js-cookie@3.0.6':
2806
  resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
2807
 
@@ -2829,11 +2838,17 @@ packages:
2829
  '@types/prop-types@15.7.14':
2830
  resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
2831
 
 
 
 
2832
  '@types/react-dom@18.3.5':
2833
  resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
2834
  peerDependencies:
2835
  '@types/react': ^18.0.0
2836
 
 
 
 
2837
  '@types/react@18.3.18':
2838
  resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
2839
 
@@ -3482,6 +3497,9 @@ packages:
3482
  resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
3483
  engines: {node: '>= 0.10'}
3484
 
 
 
 
3485
  css-tree@2.3.1:
3486
  resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
3487
  engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -4623,6 +4641,9 @@ packages:
4623
  resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
4624
  engines: {node: '>= 0.6'}
4625
 
 
 
 
4626
  merge-descriptors@1.0.3:
4627
  resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
4628
 
@@ -5029,6 +5050,10 @@ packages:
5029
  resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
5030
  engines: {node: '>=8'}
5031
 
 
 
 
 
5032
  object-inspect@1.13.3:
5033
  resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
5034
  engines: {node: '>= 0.4'}
@@ -5330,6 +5355,9 @@ packages:
5330
  resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
5331
  engines: {node: '>=10'}
5332
 
 
 
 
5333
  property-information@6.5.0:
5334
  resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
5335
 
@@ -5371,6 +5399,9 @@ packages:
5371
  queue-microtask@1.2.3:
5372
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
5373
 
 
 
 
5374
  randombytes@2.1.0:
5375
  resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
5376
 
@@ -5385,6 +5416,13 @@ packages:
5385
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5386
  engines: {node: '>= 0.8'}
5387
 
 
 
 
 
 
 
 
5388
  react-chartjs-2@5.3.0:
5389
  resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
5390
  peerDependencies:
@@ -5428,12 +5466,27 @@ packages:
5428
  react-is@16.13.1:
5429
  resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
5430
 
 
 
 
5431
  react-markdown@9.0.3:
5432
  resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
5433
  peerDependencies:
5434
  '@types/react': '>=18'
5435
  react: '>=18'
5436
 
 
 
 
 
 
 
 
 
 
 
 
 
5437
  react-refresh@0.14.2:
5438
  resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
5439
  engines: {node: '>=0.10.0'}
@@ -6090,6 +6143,9 @@ packages:
6090
  resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
6091
  engines: {node: '>=0.6.0'}
6092
 
 
 
 
6093
  tinybench@2.9.0:
6094
  resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
6095
 
@@ -6306,6 +6362,11 @@ packages:
6306
  '@types/react':
6307
  optional: true
6308
 
 
 
 
 
 
6309
  use-sidecar@1.1.3:
6310
  resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
6311
  engines: {node: '>=10'}
@@ -9306,6 +9367,11 @@ snapshots:
9306
  dependencies:
9307
  '@types/unist': 3.0.3
9308
 
 
 
 
 
 
9309
  '@types/js-cookie@3.0.6': {}
9310
 
9311
  '@types/json-schema@7.0.15': {}
@@ -9330,10 +9396,21 @@ snapshots:
9330
 
9331
  '@types/prop-types@15.7.14': {}
9332
 
 
 
 
 
9333
  '@types/react-dom@18.3.5(@types/react@18.3.18)':
9334
  dependencies:
9335
  '@types/react': 18.3.18
9336
 
 
 
 
 
 
 
 
9337
  '@types/react@18.3.18':
9338
  dependencies:
9339
  '@types/prop-types': 15.7.14
@@ -10171,6 +10248,10 @@ snapshots:
10171
  randombytes: 2.1.0
10172
  randomfill: 1.0.4
10173
 
 
 
 
 
10174
  css-tree@2.3.1:
10175
  dependencies:
10176
  mdn-data: 2.0.30
@@ -11633,6 +11714,8 @@ snapshots:
11633
 
11634
  media-typer@0.3.0: {}
11635
 
 
 
11636
  merge-descriptors@1.0.3: {}
11637
 
11638
  merge-stream@2.0.0: {}
@@ -12275,6 +12358,8 @@ snapshots:
12275
  dependencies:
12276
  path-key: 3.1.1
12277
 
 
 
12278
  object-inspect@1.13.3: {}
12279
 
12280
  object-is@1.1.6:
@@ -12566,6 +12651,12 @@ snapshots:
12566
  err-code: 2.0.3
12567
  retry: 0.12.0
12568
 
 
 
 
 
 
 
12569
  property-information@6.5.0: {}
12570
 
12571
  proxy-addr@2.0.7:
@@ -12614,6 +12705,8 @@ snapshots:
12614
 
12615
  queue-microtask@1.2.3: {}
12616
 
 
 
12617
  randombytes@2.1.0:
12618
  dependencies:
12619
  safe-buffer: 5.2.1
@@ -12632,6 +12725,20 @@ snapshots:
12632
  iconv-lite: 0.4.24
12633
  unpipe: 1.0.0
12634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12635
  react-chartjs-2@5.3.0(chart.js@4.4.7)(react@18.3.1):
12636
  dependencies:
12637
  chart.js: 4.4.7
@@ -12641,7 +12748,7 @@ snapshots:
12641
  dependencies:
12642
  dnd-core: 16.0.1
12643
 
12644
- react-dnd@16.0.1(@types/node@22.10.10)(@types/react@18.3.18)(react@18.3.1):
12645
  dependencies:
12646
  '@react-dnd/invariant': 4.0.2
12647
  '@react-dnd/shallowequal': 4.0.2
@@ -12650,6 +12757,7 @@ snapshots:
12650
  hoist-non-react-statics: 3.3.2
12651
  react: 18.3.1
12652
  optionalDependencies:
 
12653
  '@types/node': 22.10.10
12654
  '@types/react': 18.3.18
12655
 
@@ -12670,6 +12778,8 @@ snapshots:
12670
 
12671
  react-is@16.13.1: {}
12672
 
 
 
12673
  react-markdown@9.0.3(@types/react@18.3.18)(react@18.3.1):
12674
  dependencies:
12675
  '@types/hast': 3.0.4
@@ -12687,6 +12797,18 @@ snapshots:
12687
  transitivePeerDependencies:
12688
  - supports-color
12689
 
 
 
 
 
 
 
 
 
 
 
 
 
12690
  react-refresh@0.14.2: {}
12691
 
12692
  react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@18.3.1):
@@ -13414,6 +13536,8 @@ snapshots:
13414
  dependencies:
13415
  setimmediate: 1.0.5
13416
 
 
 
13417
  tinybench@2.9.0: {}
13418
 
13419
  tinyexec@0.3.2: {}
@@ -13656,6 +13780,10 @@ snapshots:
13656
  optionalDependencies:
13657
  '@types/react': 18.3.18
13658
 
 
 
 
 
13659
  use-sidecar@1.1.3(@types/react@18.3.18)(react@18.3.1):
13660
  dependencies:
13661
  detect-node-es: 1.1.0
 
152
  '@remix-run/react':
153
  specifier: ^2.15.2
154
  version: 2.15.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3)
155
+ '@types/react-beautiful-dnd':
156
+ specifier: ^13.1.8
157
+ version: 13.1.8
158
  '@uiw/codemirror-theme-vscode':
159
  specifier: ^4.23.6
160
  version: 4.23.7(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)
 
239
  react:
240
  specifier: ^18.3.1
241
  version: 18.3.1
242
+ react-beautiful-dnd:
243
+ specifier: ^13.1.1
244
+ version: 13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
245
  react-chartjs-2:
246
  specifier: ^5.3.0
247
  version: 5.3.0(chart.js@4.4.7)(react@18.3.1)
248
  react-dnd:
249
  specifier: ^16.0.1
250
+ 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)
251
  react-dnd-html5-backend:
252
  specifier: ^16.0.1
253
  version: 16.0.1
 
2808
  '@types/hast@3.0.4':
2809
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
2810
 
2811
+ '@types/hoist-non-react-statics@3.3.6':
2812
+ resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==}
2813
+
2814
  '@types/js-cookie@3.0.6':
2815
  resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
2816
 
 
2838
  '@types/prop-types@15.7.14':
2839
  resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
2840
 
2841
+ '@types/react-beautiful-dnd@13.1.8':
2842
+ resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
2843
+
2844
  '@types/react-dom@18.3.5':
2845
  resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
2846
  peerDependencies:
2847
  '@types/react': ^18.0.0
2848
 
2849
+ '@types/react-redux@7.1.34':
2850
+ resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==}
2851
+
2852
  '@types/react@18.3.18':
2853
  resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
2854
 
 
3497
  resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
3498
  engines: {node: '>= 0.10'}
3499
 
3500
+ css-box-model@1.2.1:
3501
+ resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
3502
+
3503
  css-tree@2.3.1:
3504
  resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
3505
  engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
 
4641
  resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
4642
  engines: {node: '>= 0.6'}
4643
 
4644
+ memoize-one@5.2.1:
4645
+ resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
4646
+
4647
  merge-descriptors@1.0.3:
4648
  resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
4649
 
 
5050
  resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
5051
  engines: {node: '>=8'}
5052
 
5053
+ object-assign@4.1.1:
5054
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
5055
+ engines: {node: '>=0.10.0'}
5056
+
5057
  object-inspect@1.13.3:
5058
  resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
5059
  engines: {node: '>= 0.4'}
 
5355
  resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
5356
  engines: {node: '>=10'}
5357
 
5358
+ prop-types@15.8.1:
5359
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
5360
+
5361
  property-information@6.5.0:
5362
  resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
5363
 
 
5399
  queue-microtask@1.2.3:
5400
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
5401
 
5402
+ raf-schd@4.0.3:
5403
+ resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
5404
+
5405
  randombytes@2.1.0:
5406
  resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
5407
 
 
5416
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5417
  engines: {node: '>= 0.8'}
5418
 
5419
+ react-beautiful-dnd@13.1.1:
5420
+ resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
5421
+ deprecated: 'react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672'
5422
+ peerDependencies:
5423
+ react: ^16.8.5 || ^17.0.0 || ^18.0.0
5424
+ react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
5425
+
5426
  react-chartjs-2@5.3.0:
5427
  resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
5428
  peerDependencies:
 
5466
  react-is@16.13.1:
5467
  resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
5468
 
5469
+ react-is@17.0.2:
5470
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
5471
+
5472
  react-markdown@9.0.3:
5473
  resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
5474
  peerDependencies:
5475
  '@types/react': '>=18'
5476
  react: '>=18'
5477
 
5478
+ react-redux@7.2.9:
5479
+ resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
5480
+ peerDependencies:
5481
+ react: ^16.8.3 || ^17 || ^18
5482
+ react-dom: '*'
5483
+ react-native: '*'
5484
+ peerDependenciesMeta:
5485
+ react-dom:
5486
+ optional: true
5487
+ react-native:
5488
+ optional: true
5489
+
5490
  react-refresh@0.14.2:
5491
  resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
5492
  engines: {node: '>=0.10.0'}
 
6143
  resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
6144
  engines: {node: '>=0.6.0'}
6145
 
6146
+ tiny-invariant@1.3.3:
6147
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
6148
+
6149
  tinybench@2.9.0:
6150
  resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
6151
 
 
6362
  '@types/react':
6363
  optional: true
6364
 
6365
+ use-memo-one@1.1.3:
6366
+ resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
6367
+ peerDependencies:
6368
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
6369
+
6370
  use-sidecar@1.1.3:
6371
  resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
6372
  engines: {node: '>=10'}
 
9367
  dependencies:
9368
  '@types/unist': 3.0.3
9369
 
9370
+ '@types/hoist-non-react-statics@3.3.6':
9371
+ dependencies:
9372
+ '@types/react': 18.3.18
9373
+ hoist-non-react-statics: 3.3.2
9374
+
9375
  '@types/js-cookie@3.0.6': {}
9376
 
9377
  '@types/json-schema@7.0.15': {}
 
9396
 
9397
  '@types/prop-types@15.7.14': {}
9398
 
9399
+ '@types/react-beautiful-dnd@13.1.8':
9400
+ dependencies:
9401
+ '@types/react': 18.3.18
9402
+
9403
  '@types/react-dom@18.3.5(@types/react@18.3.18)':
9404
  dependencies:
9405
  '@types/react': 18.3.18
9406
 
9407
+ '@types/react-redux@7.1.34':
9408
+ dependencies:
9409
+ '@types/hoist-non-react-statics': 3.3.6
9410
+ '@types/react': 18.3.18
9411
+ hoist-non-react-statics: 3.3.2
9412
+ redux: 4.2.1
9413
+
9414
  '@types/react@18.3.18':
9415
  dependencies:
9416
  '@types/prop-types': 15.7.14
 
10248
  randombytes: 2.1.0
10249
  randomfill: 1.0.4
10250
 
10251
+ css-box-model@1.2.1:
10252
+ dependencies:
10253
+ tiny-invariant: 1.3.3
10254
+
10255
  css-tree@2.3.1:
10256
  dependencies:
10257
  mdn-data: 2.0.30
 
11714
 
11715
  media-typer@0.3.0: {}
11716
 
11717
+ memoize-one@5.2.1: {}
11718
+
11719
  merge-descriptors@1.0.3: {}
11720
 
11721
  merge-stream@2.0.0: {}
 
12358
  dependencies:
12359
  path-key: 3.1.1
12360
 
12361
+ object-assign@4.1.1: {}
12362
+
12363
  object-inspect@1.13.3: {}
12364
 
12365
  object-is@1.1.6:
 
12651
  err-code: 2.0.3
12652
  retry: 0.12.0
12653
 
12654
+ prop-types@15.8.1:
12655
+ dependencies:
12656
+ loose-envify: 1.4.0
12657
+ object-assign: 4.1.1
12658
+ react-is: 16.13.1
12659
+
12660
  property-information@6.5.0: {}
12661
 
12662
  proxy-addr@2.0.7:
 
12705
 
12706
  queue-microtask@1.2.3: {}
12707
 
12708
+ raf-schd@4.0.3: {}
12709
+
12710
  randombytes@2.1.0:
12711
  dependencies:
12712
  safe-buffer: 5.2.1
 
12725
  iconv-lite: 0.4.24
12726
  unpipe: 1.0.0
12727
 
12728
+ react-beautiful-dnd@13.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
12729
+ dependencies:
12730
+ '@babel/runtime': 7.26.7
12731
+ css-box-model: 1.2.1
12732
+ memoize-one: 5.2.1
12733
+ raf-schd: 4.0.3
12734
+ react: 18.3.1
12735
+ react-dom: 18.3.1(react@18.3.1)
12736
+ react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
12737
+ redux: 4.2.1
12738
+ use-memo-one: 1.1.3(react@18.3.1)
12739
+ transitivePeerDependencies:
12740
+ - react-native
12741
+
12742
  react-chartjs-2@5.3.0(chart.js@4.4.7)(react@18.3.1):
12743
  dependencies:
12744
  chart.js: 4.4.7
 
12748
  dependencies:
12749
  dnd-core: 16.0.1
12750
 
12751
+ 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):
12752
  dependencies:
12753
  '@react-dnd/invariant': 4.0.2
12754
  '@react-dnd/shallowequal': 4.0.2
 
12757
  hoist-non-react-statics: 3.3.2
12758
  react: 18.3.1
12759
  optionalDependencies:
12760
+ '@types/hoist-non-react-statics': 3.3.6
12761
  '@types/node': 22.10.10
12762
  '@types/react': 18.3.18
12763
 
 
12778
 
12779
  react-is@16.13.1: {}
12780
 
12781
+ react-is@17.0.2: {}
12782
+
12783
  react-markdown@9.0.3(@types/react@18.3.18)(react@18.3.1):
12784
  dependencies:
12785
  '@types/hast': 3.0.4
 
12797
  transitivePeerDependencies:
12798
  - supports-color
12799
 
12800
+ react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
12801
+ dependencies:
12802
+ '@babel/runtime': 7.26.7
12803
+ '@types/react-redux': 7.1.34
12804
+ hoist-non-react-statics: 3.3.2
12805
+ loose-envify: 1.4.0
12806
+ prop-types: 15.8.1
12807
+ react: 18.3.1
12808
+ react-is: 17.0.2
12809
+ optionalDependencies:
12810
+ react-dom: 18.3.1(react@18.3.1)
12811
+
12812
  react-refresh@0.14.2: {}
12813
 
12814
  react-remove-scroll-bar@2.3.8(@types/react@18.3.18)(react@18.3.1):
 
13536
  dependencies:
13537
  setimmediate: 1.0.5
13538
 
13539
+ tiny-invariant@1.3.3: {}
13540
+
13541
  tinybench@2.9.0: {}
13542
 
13543
  tinyexec@0.3.2: {}
 
13780
  optionalDependencies:
13781
  '@types/react': 18.3.18
13782
 
13783
+ use-memo-one@1.1.3(react@18.3.1):
13784
+ dependencies:
13785
+ react: 18.3.1
13786
+
13787
  use-sidecar@1.1.3(@types/react@18.3.18)(react@18.3.1):
13788
  dependencies:
13789
  detect-node-es: 1.1.0