refactor: remove developer mode and related components

add glowing effect component for tab tiles
improve tab tile appearance with new glow effect
add 'none' log level and simplify log level handling
simplify tab configuration store by removing developer tabs
remove useDebugStatus hook and related debug functionality
remove system info endpoints no longer needed
This commit is contained in:
KevIsDev
2025-07-01 14:26:42 +01:00
committed by Roamin
parent 22cb5977be
commit 590363cf6e
23 changed files with 370 additions and 5608 deletions

View File

@@ -36,7 +36,7 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
/>
) : (
<div className="w-full h-full rounded-full flex items-center justify-center bg-white dark:bg-gray-800 text-gray-400 dark:text-gray-500">
<div className="i-ph:question w-6 h-6" />
<div className="i-ph:user w-6 h-6" />
</div>
)}
</motion.button>
@@ -72,7 +72,7 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 dark:text-gray-500 font-medium text-lg">
<span className="relative -top-0.5">?</span>
<div className="i-ph:user w-6 h-6" />
</div>
)}
</div>
@@ -117,24 +117,6 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
</DropdownMenu.Item>
<div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
<DropdownMenu.Item
className={classNames(
'flex items-center gap-2 px-4 py-2.5',
'text-sm text-gray-700 dark:text-gray-200',
'hover:bg-purple-50 dark:hover:bg-purple-500/10',
'hover:text-purple-500 dark:hover:text-purple-400',
'cursor-pointer transition-all duration-200',
'outline-none',
'group',
)}
onClick={() => onSelectTab('task-manager')}
>
<div className="i-ph:activity w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
Task Manager
<BetaLabel />
</DropdownMenu.Item>
<DropdownMenu.Item
className={classNames(
'flex items-center gap-2 px-4 py-2.5',

View File

@@ -1,25 +1,16 @@
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 { classNames } from '~/utils/classNames';
import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
import { TabTile } from '~/components/@settings/shared/components/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,
resetTabConfiguration,
} from '~/lib/stores/settings';
import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
import { profileStore } from '~/lib/stores/profile';
import type { TabType, TabVisibilityConfig, Profile } from './types';
import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
import type { TabType, Profile } from './types';
import { TAB_LABELS, DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
import { DialogTitle } from '~/components/ui/Dialog';
import { AvatarDropdown } from './AvatarDropdown';
import BackgroundRays from '~/components/ui/BackgroundRays';
@@ -30,14 +21,12 @@ import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
import { DataTab } from '~/components/@settings/tabs/data/DataTab';
import DebugTab from '~/components/@settings/tabs/debug/DebugTab';
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
import McpTab from '~/components/@settings/tabs/mcp/McpTab';
interface ControlPanelProps {
@@ -45,48 +34,8 @@ interface ControlPanelProps {
onClose: () => void;
}
interface TabWithDevType extends TabVisibilityConfig {
isExtraDevTab?: boolean;
}
interface ExtendedTabConfig extends TabVisibilityConfig {
isExtraDevTab?: boolean;
}
interface BaseTabConfig {
id: TabType;
visible: boolean;
window: 'user' | 'developer';
order: number;
}
interface AnimatedSwitchProps {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
id: string;
label: string;
}
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',
'tab-management': 'Configure visible tabs and their order',
mcp: 'Configure MCP (Model Context Protocol) servers',
};
// Beta status for experimental features
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
const BETA_TABS = new Set<TabType>(['service-status', 'update', 'local-providers']);
const BetaLabel = () => (
<div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
@@ -94,66 +43,6 @@ const BetaLabel = () => (
</div>
);
const AnimatedSwitch = ({ checked, onCheckedChange, id, label }: AnimatedSwitchProps) => {
return (
<div className="flex items-center gap-2">
<Switch
id={id}
checked={checked}
onCheckedChange={onCheckedChange}
className={classNames(
'relative inline-flex h-6 w-11 items-center rounded-full',
'transition-all duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)]',
'bg-gray-200 dark:bg-gray-700',
'data-[state=checked]:bg-purple-500',
'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
'cursor-pointer',
'group',
)}
>
<motion.span
className={classNames(
'absolute left-[2px] top-[2px]',
'inline-block h-5 w-5 rounded-full',
'bg-white shadow-lg',
'transition-shadow duration-300',
'group-hover:shadow-md group-active:shadow-sm',
'group-hover:scale-95 group-active:scale-90',
)}
initial={false}
transition={{
type: 'spring',
stiffness: 500,
damping: 30,
duration: 0.2,
}}
animate={{
x: checked ? '1.25rem' : '0rem',
}}
>
<motion.div
className="absolute inset-0 rounded-full bg-white"
initial={false}
animate={{
scale: checked ? 1 : 0.8,
}}
transition={{ duration: 0.2 }}
/>
</motion.span>
<span className="sr-only">Toggle {label}</span>
</Switch>
<div className="flex items-center gap-2">
<label
htmlFor={id}
className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
>
{label}
</label>
</div>
</div>
);
};
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
// State
const [activeTab, setActiveTab] = useState<TabType | null>(null);
@@ -162,7 +51,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
// Store values
const tabConfiguration = useStore(tabConfigurationStore);
const developerMode = useStore(developerModeStore);
const profile = useStore(profileStore) as Profile;
// Status hooks
@@ -170,7 +58,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
// Memoize the base tab configurations to avoid recalculation
const baseTabConfig = useMemo(() => {
@@ -188,41 +75,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
const notificationsDisabled = profile?.preferences?.notifications === false;
// In developer mode, show ALL tabs without restrictions
if (developerMode) {
const seenTabs = new Set<TabType>();
const devTabs: ExtendedTabConfig[] = [];
// Process tabs in order of priority: developer, user, default
const processTab = (tab: BaseTabConfig) => {
if (!seenTabs.has(tab.id)) {
seenTabs.add(tab.id);
devTabs.push({
id: tab.id,
visible: true,
window: 'developer',
order: tab.order || devTabs.length,
});
}
};
// Process tabs in priority order
tabConfiguration.developerTabs?.forEach((tab) => processTab(tab as BaseTabConfig));
tabConfiguration.userTabs.forEach((tab) => processTab(tab as BaseTabConfig));
DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
// Add Tab Management tile
devTabs.push({
id: 'tab-management' as TabType,
visible: true,
window: 'developer',
order: devTabs.length,
isExtraDevTab: true,
});
return devTabs.sort((a, b) => a.order - b.order);
}
// Optimize user mode tab filtering
return tabConfiguration.userTabs
.filter((tab) => {
@@ -237,33 +89,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
return tab.visible && tab.window === 'user';
})
.sort((a, b) => a.order - b.order);
}, [tabConfiguration, developerMode, profile?.preferences?.notifications, baseTabConfig]);
// Optimize animation performance with layout animations
const gridLayoutVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
delayChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, scale: 0.8 },
visible: {
opacity: 1,
scale: 1,
transition: {
type: 'spring',
stiffness: 200,
damping: 20,
mass: 0.6,
},
},
};
}, [tabConfiguration, profile?.preferences?.notifications, baseTabConfig]);
// Reset to default view when modal opens/closes
useEffect(() => {
@@ -295,21 +121,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
}
};
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 = (tabId: TabType | 'tab-management') => {
if (tabId === 'tab-management') {
return <TabManagement />;
}
const getTabComponent = (tabId: TabType) => {
switch (tabId) {
case 'profile':
return <ProfileTab />;
@@ -327,14 +139,10 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
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 />;
case 'mcp':
@@ -354,8 +162,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
return hasUnreadNotifications;
case 'connection':
return hasConnectionIssues;
case 'debug':
return hasActiveWarnings;
default:
return false;
}
@@ -375,12 +181,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
: 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 '';
}
@@ -405,9 +205,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
case 'connection':
acknowledgeIssue();
break;
case 'debug':
acknowledgeAllIssues();
break;
}
// Clear loading state after a delay
@@ -418,15 +215,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
<RadixDialog.Root open={open}>
<RadixDialog.Portal>
<div className="fixed inset-0 flex items-center justify-center z-[100] modern-scrollbar">
<RadixDialog.Overlay asChild>
<motion.div
className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
</RadixDialog.Overlay>
<RadixDialog.Overlay className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm transition-opacity duration-200" />
<RadixDialog.Content
aria-describedby={undefined}
@@ -434,19 +223,17 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
onPointerDownOutside={handleClose}
className="relative z-[101]"
>
<motion.div
<div
className={classNames(
'w-[1200px] h-[90vh]',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'bg-bolt-elements-background-depth-1',
'rounded-2xl shadow-2xl',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'border border-bolt-elements-borderColor',
'flex flex-col overflow-hidden',
'relative',
'transform transition-all duration-200 ease-out',
open ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-95 translate-y-4',
)}
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 }}
>
<div className="absolute inset-0 overflow-hidden rounded-2xl">
<BackgroundRays />
@@ -458,7 +245,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
{(activeTab || showTabManagement) && (
<button
onClick={handleBack}
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-colors duration-150"
>
<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>
@@ -469,18 +256,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
</div>
<div className="flex items-center gap-6">
{/* Mode Toggle */}
<div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
<AnimatedSwitch
id="developer-mode"
checked={developerMode}
onCheckedChange={handleDeveloperModeChange}
label={developerMode ? 'Developer Mode' : 'User Mode'}
/>
</div>
{/* Avatar and Dropdown */}
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
<div className="pl-6">
<AvatarDropdown onSelectTab={handleTabClick} />
</div>
@@ -508,49 +285,48 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
'touch-auto',
)}
>
<motion.div
key={activeTab || 'home'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="p-6"
<div
className={classNames(
'p-6 transition-opacity duration-150',
activeTab || showTabManagement ? 'opacity-100' : 'opacity-100',
)}
>
{showTabManagement ? (
<TabManagement />
) : activeTab ? (
{activeTab ? (
getTabComponent(activeTab)
) : (
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative"
variants={gridLayoutVariants}
initial="hidden"
animate="visible"
>
<AnimatePresence mode="popLayout">
{(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
<motion.div key={tab.id} layout variants={itemVariants} className="aspect-[1.5/1]">
<TabTile
tab={tab}
onClick={() => handleTabClick(tab.id as TabType)}
isActive={activeTab === tab.id}
hasUpdate={getTabUpdateStatus(tab.id)}
statusMessage={getStatusMessage(tab.id)}
description={TAB_DESCRIPTIONS[tab.id]}
isLoading={loadingTab === tab.id}
className="h-full relative"
>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</TabTile>
</motion.div>
))}
</AnimatePresence>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
{visibleTabs.map((tab, index) => (
<div
key={tab.id}
className={classNames(
'aspect-[1.5/1] transition-transform duration-100 ease-out',
'hover:scale-[1.01]',
)}
style={{
animationDelay: `${index * 30}ms`,
animation: open ? 'fadeInUp 200ms ease-out forwards' : 'none',
}}
>
<TabTile
tab={tab}
onClick={() => handleTabClick(tab.id as TabType)}
isActive={activeTab === tab.id}
hasUpdate={getTabUpdateStatus(tab.id)}
statusMessage={getStatusMessage(tab.id)}
description={TAB_DESCRIPTIONS[tab.id]}
isLoading={loadingTab === tab.id}
className="h-full relative"
>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</TabTile>
</div>
))}
</div>
)}
</motion.div>
</div>
</div>
</div>
</motion.div>
</div>
</RadixDialog.Content>
</div>
</RadixDialog.Portal>

View File

@@ -10,11 +10,8 @@ export const TAB_ICONS: Record<TabType, string> = {
'local-providers': 'i-ph:desktop-fill',
'service-status': 'i-ph:activity-bold',
connection: 'i-ph:wifi-high-fill',
debug: 'i-ph:bug-fill',
'event-logs': 'i-ph:list-bullets-fill',
update: 'i-ph:arrow-clockwise-fill',
'task-manager': 'i-ph:chart-line-fill',
'tab-management': 'i-ph:squares-four-fill',
mcp: 'i-ph:hard-drives-bold',
};
@@ -28,11 +25,8 @@ export const TAB_LABELS: Record<TabType, string> = {
'local-providers': 'Local Providers',
'service-status': 'Service Status',
connection: 'Connection',
debug: 'Debug',
'event-logs': 'Event Logs',
update: 'Updates',
'task-manager': 'Task Manager',
'tab-management': 'Tab Management',
mcp: 'MCP Servers',
};
@@ -46,11 +40,8 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
'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',
'tab-management': 'Configure visible tabs and their order',
mcp: 'Configure MCP (Model Context Protocol) servers',
};
@@ -61,32 +52,13 @@ export const DEFAULT_TAB_CONFIG = [
{ id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
{ id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
{ id: 'connection', visible: true, window: 'user' as const, order: 4 },
{ id: 'connection', visible: true, window: 'user' as const, order: 5 },
{ id: 'notifications', visible: true, window: 'user' as const, order: 6 },
{ id: 'notifications', visible: true, window: 'user' as const, order: 5 },
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
{ id: 'mcp', visible: true, window: 'user' as const, order: 7 },
{ id: 'profile', visible: true, window: 'user' as const, order: 8 },
{ id: 'settings', visible: true, window: 'user' as const, order: 9 },
{ id: 'service-status', visible: true, window: 'user' as const, order: 10 },
{ id: 'update', visible: true, window: 'user' as const, order: 11 },
// User Window Tabs (In dropdown, initially hidden)
{ id: 'profile', visible: false, window: 'user' as const, order: 8 },
{ id: 'settings', visible: false, window: 'user' as const, order: 9 },
{ id: 'task-manager', visible: false, window: 'user' as const, order: 10 },
{ id: 'service-status', visible: false, window: 'user' as const, order: 11 },
// User Window Tabs (Hidden, controlled by TaskManagerTab)
{ id: 'debug', visible: false, window: 'user' as const, order: 12 },
{ id: 'update', visible: false, window: 'user' as const, order: 13 },
// Developer Window Tabs (All visible by default)
{ id: 'features', visible: true, window: 'developer' as const, order: 0 },
{ id: 'data', visible: true, window: 'developer' as const, order: 1 },
{ id: 'cloud-providers', visible: true, window: 'developer' as const, order: 2 },
{ id: 'local-providers', visible: true, window: 'developer' as const, order: 3 },
{ id: 'connection', visible: true, window: 'developer' as const, order: 4 },
{ id: 'notifications', visible: true, window: 'developer' as const, order: 5 },
{ id: 'event-logs', visible: true, window: 'developer' as const, order: 6 },
{ id: 'profile', visible: true, window: 'developer' as const, order: 7 },
{ id: 'settings', visible: true, window: 'developer' as const, order: 8 },
{ id: 'task-manager', visible: true, window: 'developer' as const, order: 9 },
{ id: 'service-status', visible: true, window: 'developer' as const, order: 10 },
{ id: 'debug', visible: true, window: 'developer' as const, order: 11 },
{ id: 'update', visible: true, window: 'developer' as const, order: 12 },
];

View File

@@ -12,11 +12,8 @@ export type TabType =
| 'local-providers'
| 'service-status'
| 'connection'
| 'debug'
| 'event-logs'
| 'update'
| 'task-manager'
| 'tab-management'
| 'mcp';
export type WindowType = 'user' | 'developer';
@@ -64,7 +61,6 @@ export interface UserTabConfig extends TabVisibilityConfig {
export interface TabWindowConfig {
userTabs: UserTabConfig[];
developerTabs: DevTabConfig[];
}
export const TAB_LABELS: Record<TabType, string> = {
@@ -77,11 +73,8 @@ export const TAB_LABELS: Record<TabType, string> = {
'local-providers': 'Local Providers',
'service-status': 'Service Status',
connection: 'Connections',
debug: 'Debug',
'event-logs': 'Event Logs',
update: 'Updates',
'task-manager': 'Task Manager',
'tab-management': 'Tab Management',
mcp: 'MCP Servers',
};

View File

@@ -7,8 +7,6 @@ export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constan
// Shared components
export { TabTile } from './shared/components/TabTile';
export { TabManagement } from './shared/components/TabManagement';
// Utils
export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
export * from './utils/animations';

View File

@@ -1,382 +0,0 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { useStore } from '@nanostores/react';
import { Switch } from '~/components/ui/Switch';
import { classNames } from '~/utils/classNames';
import { tabConfigurationStore } from '~/lib/stores/settings';
import { TAB_LABELS } from '~/components/@settings/core/constants';
import type { TabType } from '~/components/@settings/core/types';
import { toast } from 'react-toastify';
import { TbLayoutGrid } from 'react-icons/tb';
import { useSettingsStore } from '~/lib/stores/settings';
// Define tab icons mapping
const TAB_ICONS: Record<TabType, string> = {
profile: 'i-ph:user-circle-fill',
settings: 'i-ph:gear-six-fill',
notifications: 'i-ph:bell-fill',
features: 'i-ph:star-fill',
data: 'i-ph:database-fill',
'cloud-providers': 'i-ph:cloud-fill',
'local-providers': 'i-ph:desktop-fill',
'service-status': 'i-ph:activity-fill',
connection: 'i-ph:wifi-high-fill',
debug: 'i-ph:bug-fill',
'event-logs': 'i-ph:list-bullets-fill',
update: 'i-ph:arrow-clockwise-fill',
'task-manager': 'i-ph:chart-line-fill',
'tab-management': 'i-ph:squares-four-fill',
mcp: 'i-ph:hard-drives-bold',
};
// Define which tabs are default in user mode
const DEFAULT_USER_TABS: TabType[] = [
'features',
'data',
'cloud-providers',
'local-providers',
'connection',
'notifications',
'event-logs',
'mcp',
];
// Define which tabs can be added to user mode
const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 'service-status', 'debug', 'update'];
// All available tabs for user mode
const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS];
// Define which tabs are beta
const BETA_TABS = new Set<TabType>(['task-manager', 'service-status', 'update', 'local-providers']);
// Beta label component
const BetaLabel = () => (
<span className="px-1.5 py-0.5 text-[10px] rounded-full bg-purple-500/10 text-purple-500 font-medium">BETA</span>
);
export const TabManagement = () => {
const [searchQuery, setSearchQuery] = useState('');
const tabConfiguration = useStore(tabConfigurationStore);
const { setSelectedTab } = useSettingsStore();
const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
// Get current tab configuration
const currentTab = tabConfiguration.userTabs.find((tab) => tab.id === tabId);
// If tab doesn't exist in configuration, create it
if (!currentTab) {
const newTab = {
id: tabId,
visible: checked,
window: 'user' as const,
order: tabConfiguration.userTabs.length,
};
const updatedTabs = [...tabConfiguration.userTabs, newTab];
tabConfigurationStore.set({
...tabConfiguration,
userTabs: updatedTabs,
});
toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
return;
}
// Check if tab can be enabled in user mode
const canBeEnabled = DEFAULT_USER_TABS.includes(tabId) || OPTIONAL_USER_TABS.includes(tabId);
if (!canBeEnabled && checked) {
toast.error('This tab cannot be enabled in user mode');
return;
}
// Update tab visibility
const updatedTabs = tabConfiguration.userTabs.map((tab) => {
if (tab.id === tabId) {
return { ...tab, visible: checked };
}
return tab;
});
// Update store
tabConfigurationStore.set({
...tabConfiguration,
userTabs: updatedTabs,
});
// Show success message
toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
};
// Create a map of existing tab configurations
const tabConfigMap = new Map(tabConfiguration.userTabs.map((tab) => [tab.id, tab]));
// Generate the complete list of tabs, including those not in the configuration
const allTabs = ALL_USER_TABS.map((tabId) => {
return (
tabConfigMap.get(tabId) || {
id: tabId,
visible: false,
window: 'user' as const,
order: -1,
}
);
});
// Filter tabs based on search query
const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
useEffect(() => {
// Reset to first tab when component unmounts
return () => {
setSelectedTab('user'); // Reset to user tab when unmounting
};
}, [setSelectedTab]);
return (
<div className="space-y-6">
<motion.div
className="space-y-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{/* Header */}
<div className="flex items-center justify-between gap-4 mt-8 mb-4">
<div className="flex items-center gap-2">
<div
className={classNames(
'w-8 h-8 flex items-center justify-center rounded-lg',
'bg-bolt-elements-background-depth-3',
'text-purple-500',
)}
>
<TbLayoutGrid className="w-5 h-5" />
</div>
<div>
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Tab Management</h4>
<p className="text-sm text-bolt-elements-textSecondary">Configure visible tabs and their order</p>
</div>
</div>
{/* Search */}
<div className="relative w-64">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<div className="i-ph:magnifying-glass w-4 h-4 text-gray-400" />
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search tabs..."
className={classNames(
'w-full pl-10 pr-4 py-2 rounded-lg',
'bg-bolt-elements-background-depth-2',
'border border-bolt-elements-borderColor',
'text-bolt-elements-textPrimary',
'placeholder-bolt-elements-textTertiary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
)}
/>
</div>
</div>
{/* Tab Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Default Section Header */}
{filteredTabs.some((tab) => DEFAULT_USER_TABS.includes(tab.id)) && (
<div className="col-span-full flex items-center gap-2 mt-4 mb-2">
<div className="i-ph:star-fill w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Default Tabs</span>
</div>
)}
{/* Default Tabs */}
{filteredTabs
.filter((tab) => DEFAULT_USER_TABS.includes(tab.id))
.map((tab, index) => (
<motion.div
key={tab.id}
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',
'relative overflow-hidden group',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ scale: 1.02 }}
>
{/* Status Badges */}
<div className="absolute top-1 right-1.5 flex gap-1">
<span className="px-1.5 py-0.25 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium mr-2">
Default
</span>
</div>
<div className="flex items-start gap-4 p-4">
<motion.div
className={classNames(
'w-10 h-10 flex items-center justify-center rounded-xl',
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
'transition-all duration-200',
tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div
className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
>
<div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
</div>
</motion.div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
{TAB_LABELS[tab.id]}
</h4>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</div>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
</p>
</div>
<Switch
checked={tab.visible}
onCheckedChange={(checked) => {
const isDisabled =
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id);
if (!isDisabled) {
handleTabVisibilityChange(tab.id, checked);
}
}}
className={classNames('data-[state=checked]:bg-purple-500 ml-4', {
'opacity-50 pointer-events-none':
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
})}
/>
</div>
</div>
</div>
<motion.div
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
animate={{
borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
scale: tab.visible ? 1 : 0.98,
}}
transition={{ duration: 0.2 }}
/>
</motion.div>
))}
{/* Optional Section Header */}
{filteredTabs.some((tab) => OPTIONAL_USER_TABS.includes(tab.id)) && (
<div className="col-span-full flex items-center gap-2 mt-8 mb-2">
<div className="i-ph:plus-circle-fill w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Optional Tabs</span>
</div>
)}
{/* Optional Tabs */}
{filteredTabs
.filter((tab) => OPTIONAL_USER_TABS.includes(tab.id))
.map((tab, index) => (
<motion.div
key={tab.id}
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',
'relative overflow-hidden group',
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ scale: 1.02 }}
>
{/* Status Badges */}
<div className="absolute top-1 right-1.5 flex gap-1">
<span className="px-1.5 py-0.25 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium mr-2">
Optional
</span>
</div>
<div className="flex items-start gap-4 p-4">
<motion.div
className={classNames(
'w-10 h-10 flex items-center justify-center rounded-xl',
'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
'transition-all duration-200',
tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div
className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
>
<div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
</div>
</motion.div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
{TAB_LABELS[tab.id]}
</h4>
{BETA_TABS.has(tab.id) && <BetaLabel />}
</div>
<p className="text-xs text-bolt-elements-textSecondary mt-0.5">
{tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
</p>
</div>
<Switch
checked={tab.visible}
onCheckedChange={(checked) => {
const isDisabled =
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id);
if (!isDisabled) {
handleTabVisibilityChange(tab.id, checked);
}
}}
className={classNames('data-[state=checked]:bg-purple-500 ml-4', {
'opacity-50 pointer-events-none':
!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
})}
/>
</div>
</div>
</div>
<motion.div
className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
animate={{
borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
scale: tab.visible ? 1 : 0.98,
}}
transition={{ duration: 0.2 }}
/>
</motion.div>
))}
</div>
</motion.div>
</div>
);
};

View File

@@ -1,8 +1,8 @@
import { motion } from 'framer-motion';
import * as Tooltip from '@radix-ui/react-tooltip';
import { classNames } from '~/utils/classNames';
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
import { GlowingEffect } from '~/components/ui/GlowingEffect';
interface TabTileProps {
tab: TabVisibilityConfig;
@@ -28,106 +28,118 @@ export const TabTile: React.FC<TabTileProps> = ({
children,
}: TabTileProps) => {
return (
<Tooltip.Provider delayDuration={200}>
<Tooltip.Provider delayDuration={0}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<motion.div
onClick={onClick}
className={classNames(
'relative flex flex-col items-center p-6 rounded-xl',
'w-full h-full min-h-[160px]',
'bg-white dark:bg-[#141414]',
'border border-[#E5E5E5] dark:border-[#333333]',
'group',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
isActive ? 'border-purple-500 dark:border-purple-500/50 bg-purple-500/5 dark:bg-purple-500/10' : '',
isLoading ? 'cursor-wait opacity-70' : '',
className || '',
)}
>
{/* Main Content */}
<div className="flex flex-col items-center justify-center flex-1 w-full">
{/* Icon */}
<motion.div
<div className={classNames('min-h-[160px] list-none', className || '')}>
<div className="relative h-full rounded-xl border border-[#E5E5E5] dark:border-[#333333] p-0.5">
<GlowingEffect
blur={0}
borderWidth={1}
spread={20}
glow={true}
disabled={false}
proximity={40}
inactiveZone={0.3}
movementDuration={0.4}
/>
<div
onClick={onClick}
className={classNames(
'relative',
'w-14 h-14',
'flex items-center justify-center',
'rounded-xl',
'bg-gray-100 dark:bg-gray-800',
'ring-1 ring-gray-200 dark:ring-gray-700',
'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
'relative flex flex-col items-center justify-center h-full p-4 rounded-lg',
'bg-white dark:bg-[#141414]',
'group cursor-pointer',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'transition-colors duration-100 ease-out',
isActive ? 'bg-purple-500/5 dark:bg-purple-500/10' : '',
isLoading ? 'cursor-wait opacity-70 pointer-events-none' : '',
)}
>
<motion.div
{/* Icon */}
<div
className={classNames(
TAB_ICONS[tab.id],
'w-8 h-8',
'text-gray-600 dark:text-gray-300',
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
)}
/>
</motion.div>
{/* Label and Description */}
<div className="flex flex-col items-center mt-5 w-full">
<h3
className={classNames(
'text-[15px] font-medium leading-snug mb-2',
'text-gray-700 dark:text-gray-200',
'group-hover:text-purple-600 dark:group-hover:text-purple-300/90',
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
'relative',
'w-14 h-14',
'flex items-center justify-center',
'rounded-xl',
'bg-gray-100 dark:bg-gray-800',
'ring-1 ring-gray-200 dark:ring-gray-700',
'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
'transition-all duration-100 ease-out',
isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
)}
>
{TAB_LABELS[tab.id]}
</h3>
{description && (
<p
<div
className={classNames(
'text-[13px] leading-relaxed',
'text-gray-500 dark:text-gray-400',
'max-w-[85%]',
'text-center',
'group-hover:text-purple-500 dark:group-hover:text-purple-400/70',
isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
TAB_ICONS[tab.id],
'w-8 h-8',
'text-gray-600 dark:text-gray-300',
'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
'transition-colors duration-100 ease-out',
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
)}
/>
</div>
{/* Label and Description */}
<div className="flex flex-col items-center mt-4 w-full">
<h3
className={classNames(
'text-[15px] font-medium leading-snug mb-2',
'text-gray-700 dark:text-gray-200',
'group-hover:text-purple-600 dark:group-hover:text-purple-300/90',
'transition-colors duration-100 ease-out',
isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
)}
>
{description}
</p>
{TAB_LABELS[tab.id]}
</h3>
{description && (
<p
className={classNames(
'text-[13px] leading-relaxed',
'text-gray-500 dark:text-gray-400',
'max-w-[85%]',
'text-center',
'group-hover:text-purple-500 dark:group-hover:text-purple-400/70',
'transition-colors duration-100 ease-out',
isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
)}
>
{description}
</p>
)}
</div>
{/* Update Indicator with Tooltip */}
{hasUpdate && (
<>
<div className="absolute top-4 right-4 w-2 h-2 rounded-full bg-purple-500 dark:bg-purple-400 animate-pulse" />
<Tooltip.Portal>
<Tooltip.Content
className={classNames(
'px-3 py-1.5 rounded-lg',
'bg-[#18181B] text-white',
'text-sm font-medium',
'select-none',
'z-[100]',
)}
side="top"
sideOffset={5}
>
{statusMessage}
<Tooltip.Arrow className="fill-[#18181B]" />
</Tooltip.Content>
</Tooltip.Portal>
</>
)}
{/* Children (e.g. Beta Label) */}
{children}
</div>
</div>
{/* Update Indicator with Tooltip */}
{hasUpdate && (
<>
<div className="absolute top-4 right-4 w-2 h-2 rounded-full bg-purple-500 dark:bg-purple-400 animate-pulse" />
<Tooltip.Portal>
<Tooltip.Content
className={classNames(
'px-3 py-1.5 rounded-lg',
'bg-[#18181B] text-white',
'text-sm font-medium',
'select-none',
'z-[100]',
)}
side="top"
sideOffset={5}
>
{statusMessage}
<Tooltip.Arrow className="fill-[#18181B]" />
</Tooltip.Content>
</Tooltip.Portal>
</>
)}
{/* Children (e.g. Beta Label) */}
{children}
</motion.div>
</div>
</Tooltip.Trigger>
</Tooltip.Root>
</Tooltip.Provider>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +0,0 @@
import type { Variants } from 'framer-motion';
export const fadeIn: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
export const slideIn: Variants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 },
};
export const scaleIn: Variants = {
initial: { opacity: 0, scale: 0.8 },
animate: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0.8 },
};
export const tabAnimation: Variants = {
initial: { opacity: 0, scale: 0.8, y: 20 },
animate: { opacity: 1, scale: 1, y: 0 },
exit: { opacity: 0, scale: 0.8, y: -20 },
};
export const overlayAnimation: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
export const modalAnimation: Variants = {
initial: { opacity: 0, scale: 0.95, y: 20 },
animate: { opacity: 1, scale: 1, y: 0 },
exit: { opacity: 0, scale: 0.95, y: 20 },
};
export const transition = {
duration: 0.2,
};

View File

@@ -1,9 +1,8 @@
import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types';
import type { TabVisibilityConfig } from '~/components/@settings/core/types';
import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
export const getVisibleTabs = (
tabConfiguration: { userTabs: TabVisibilityConfig[]; developerTabs?: TabVisibilityConfig[] },
isDeveloperMode: boolean,
tabConfiguration: { userTabs: TabVisibilityConfig[] },
notificationsEnabled: boolean,
): TabVisibilityConfig[] => {
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
@@ -11,35 +10,6 @@ export const getVisibleTabs = (
return DEFAULT_TAB_CONFIG as TabVisibilityConfig[];
}
// In developer mode, show ALL tabs without restrictions
if (isDeveloperMode) {
// 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),
'task-manager' as TabType, // Always include task-manager in developer mode
]);
// 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 as TabType,
visible: true,
window: 'developer' as const,
order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
} as TabVisibilityConfig;
});
return devTabs.sort((a, b) => a.order - b.order);
}
// In user mode, only show visible user tabs
return tabConfiguration.userTabs
.filter((tab) => {
@@ -53,11 +23,6 @@ export const getVisibleTabs = (
return false;
}
// Always show task-manager in user mode if it's configured as visible
if (tab.id === 'task-manager') {
return tab.visible;
}
// Only show tabs that are explicitly visible and assigned to the user window
return tab.visible && tab.window === 'user';
})

View File

@@ -0,0 +1,192 @@
import { memo, useCallback, useEffect, useRef } from 'react';
import { cn } from '~/utils/cn';
import { animate } from 'framer-motion';
interface GlowingEffectProps {
blur?: number;
inactiveZone?: number;
proximity?: number;
spread?: number;
variant?: 'default' | 'white';
glow?: boolean;
className?: string;
disabled?: boolean;
movementDuration?: number;
borderWidth?: number;
}
const GlowingEffect = memo(
({
blur = 0,
inactiveZone = 0.7,
proximity = 0,
spread = 20,
variant = 'default',
glow = false,
className,
movementDuration = 2,
borderWidth = 1,
disabled = true,
}: GlowingEffectProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const lastPosition = useRef({ x: 0, y: 0 });
const animationFrameRef = useRef<number>(0);
const handleMove = useCallback(
(e?: MouseEvent | { x: number; y: number }) => {
if (!containerRef.current) {
return;
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const element = containerRef.current;
if (!element) {
return;
}
const { left, top, width, height } = element.getBoundingClientRect();
const mouseX = e?.x ?? lastPosition.current.x;
const mouseY = e?.y ?? lastPosition.current.y;
if (e) {
lastPosition.current = { x: mouseX, y: mouseY };
}
const center = [left + width * 0.5, top + height * 0.5];
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]);
const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone;
if (distanceFromCenter < inactiveRadius) {
element.style.setProperty('--active', '0');
return;
}
const isActive =
mouseX > left - proximity &&
mouseX < left + width + proximity &&
mouseY > top - proximity &&
mouseY < top + height + proximity;
element.style.setProperty('--active', isActive ? '1' : '0');
if (!isActive) {
return;
}
const currentAngle = parseFloat(element.style.getPropertyValue('--start')) || 0;
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90;
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
const newAngle = currentAngle + angleDiff;
animate(currentAngle, newAngle, {
duration: movementDuration,
ease: [0.16, 1, 0.3, 1],
onUpdate: (value) => {
element.style.setProperty('--start', String(value));
},
});
});
},
[inactiveZone, proximity, movementDuration],
);
useEffect(() => {
if (disabled) {
return undefined;
}
const handleScroll = () => handleMove();
const handlePointerMove = (e: PointerEvent) => handleMove(e);
window.addEventListener('scroll', handleScroll, { passive: true });
document.body.addEventListener('pointermove', handlePointerMove, {
passive: true,
});
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
window.removeEventListener('scroll', handleScroll);
document.body.removeEventListener('pointermove', handlePointerMove);
};
}, [handleMove, disabled]);
return (
<>
<div
className={cn(
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
glow && 'opacity-100',
variant === 'white' && 'border-white',
disabled && '!block',
)}
/>
<div
ref={containerRef}
style={
{
'--blur': `${blur}px`,
'--spread': spread,
'--start': '0',
'--active': '0',
'--glowingeffect-border-width': `${borderWidth}px`,
'--repeating-conic-gradient-times': '5',
'--gradient':
variant === 'white'
? `repeating-conic-gradient(
from 236.84deg at 50% 50%,
var(--black),
var(--black) calc(25% / var(--repeating-conic-gradient-times))
)`
: `radial-gradient(circle, #9333ea 10%, #9333ea00 20%),
radial-gradient(circle at 40% 40%, #a855f7 5%, #a855f700 15%),
radial-gradient(circle at 60% 60%, #8b5cf6 10%, #8b5cf600 20%),
radial-gradient(circle at 40% 60%, #f63bdd 10%, #3b82f600 20%),
repeating-conic-gradient(
from 236.84deg at 50% 50%,
#9333ea 0%,
#a855f7 calc(25% / var(--repeating-conic-gradient-times)),
#8b5cf6 calc(50% / var(--repeating-conic-gradient-times)),
#f63bdd calc(75% / var(--repeating-conic-gradient-times)),
#9333ea calc(100% / var(--repeating-conic-gradient-times))
)`,
} as React.CSSProperties
}
className={cn(
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
glow && 'opacity-100',
blur > 0 && 'blur-[var(--blur)] ',
className,
disabled && '!hidden',
)}
>
<div
className={cn(
'glow',
'rounded-[inherit]',
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]',
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
'after:[background:var(--gradient)] after:[background-attachment:fixed]',
'after:opacity-[var(--active)] after:transition-opacity after:duration-300',
'after:[mask-clip:padding-box,border-box]',
'after:[mask-composite:intersect]',
'after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
)}
/>
</div>
</>
);
},
);
GlowingEffect.displayName = 'GlowingEffect';
export { GlowingEffect };