refactor: repartition server-side and client-side code
This commit is contained in:
140
app/.client/components/@settings/core/AvatarDropdown.tsx
Normal file
140
app/.client/components/@settings/core/AvatarDropdown.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useMemo } from 'react';
|
||||
import { useAuth } from '~/.client/hooks/useAuth';
|
||||
import type { TabType } from './types';
|
||||
|
||||
interface AvatarDropdownProps {
|
||||
onSelectTab: (tab: TabType) => void;
|
||||
}
|
||||
|
||||
export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
|
||||
const { userInfo, isAuthenticated } = useAuth();
|
||||
|
||||
const displayName = useMemo(() => {
|
||||
if (!isAuthenticated || !userInfo) {
|
||||
return 'Guest User';
|
||||
}
|
||||
|
||||
return userInfo.name || userInfo.username;
|
||||
}, [userInfo]);
|
||||
|
||||
const contactInfo = useMemo(() => {
|
||||
if (!isAuthenticated || !userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (userInfo.phone_number) {
|
||||
return `+${userInfo.phone_number}`;
|
||||
}
|
||||
|
||||
return userInfo.email;
|
||||
}, [userInfo]);
|
||||
|
||||
const avatarUrl = isAuthenticated && userInfo?.picture ? userInfo.picture : '';
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<motion.button
|
||||
className="size-10 rounded-full bg-transparent flex items-center justify-center focus:outline-none"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={displayName}
|
||||
className="size-full rounded-full object-cover"
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-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 size-6" />
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className={classNames(
|
||||
'min-w-[240px] z-[250]',
|
||||
'bg-white dark:bg-[#141414]',
|
||||
'rounded-lg shadow-lg',
|
||||
'border border-gray-200/50 dark:border-gray-800/50',
|
||||
'animate-in fade-in-0 zoom-in-95',
|
||||
'py-1',
|
||||
)}
|
||||
sideOffset={5}
|
||||
align="end"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'px-4 py-3 flex items-center gap-3',
|
||||
'border-b border-gray-200/50 dark:border-gray-800/50',
|
||||
)}
|
||||
>
|
||||
<div className="size-10 rounded-full overflow-hidden flex-shrink-0 bg-white dark:bg-gray-800 shadow-sm">
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={displayName}
|
||||
className={classNames('size-full', 'object-cover', 'transform-gpu', 'image-rendering-crisp')}
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-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>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-white truncate">{displayName}</div>
|
||||
{isAuthenticated && userInfo?.email && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{contactInfo}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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('settings')}
|
||||
>
|
||||
<div className="i-ph:gear-six size-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
||||
Settings
|
||||
</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 size-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
|
||||
Task Manager
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
331
app/.client/components/@settings/core/ControlPanel.tsx
Normal file
331
app/.client/components/@settings/core/ControlPanel.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import classNames from 'classnames';
|
||||
import { AnimatePresence, motion, type Variants } from 'framer-motion';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { TabTile } from '~/.client/components/@settings/core/TabTile';
|
||||
import DebugTab from '~/.client/components/@settings/tabs/debug/DebugTab';
|
||||
import { EventLogsTab } from '~/.client/components/@settings/tabs/event-logs/EventLogsTab';
|
||||
import NotificationsTab from '~/.client/components/@settings/tabs/notifications/NotificationsTab';
|
||||
import SettingsTab from '~/.client/components/@settings/tabs/settings/SettingsTab';
|
||||
import TaskManagerTab from '~/.client/components/@settings/tabs/task-manager/TaskManagerTab';
|
||||
import BackgroundRays from '~/.client/components/ui/BackgroundRays';
|
||||
import { useDebugStatus } from '~/.client/hooks/useDebugStatus';
|
||||
import { useNotifications } from '~/.client/hooks/useNotifications';
|
||||
import { profileStore } from '~/.client/stores/profile';
|
||||
import { resetTabConfiguration, tabConfigurationStore } from '~/.client/stores/settings';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { AvatarDropdown } from './AvatarDropdown';
|
||||
import { DEFAULT_TAB_CONFIG, TAB_DESCRIPTIONS } from './constants';
|
||||
import type { Profile, TabType, TabVisibilityConfig } from './types';
|
||||
|
||||
const logger = createScopedLogger('ControlPanel');
|
||||
|
||||
interface ControlPanelProps {
|
||||
open: boolean;
|
||||
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;
|
||||
}
|
||||
|
||||
export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
|
||||
// State
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
||||
|
||||
// Store values
|
||||
const tabConfiguration = useStore(tabConfigurationStore);
|
||||
const profile = useStore(profileStore) as Profile;
|
||||
|
||||
// Status hooks
|
||||
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
||||
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
||||
|
||||
// Memoize the base tab configurations to avoid recalculation
|
||||
const baseTabConfig = useMemo(() => {
|
||||
return new Map(DEFAULT_TAB_CONFIG.map((tab) => [tab.id, tab]));
|
||||
}, []);
|
||||
|
||||
// Add visibleTabs logic using useMemo with optimized calculations
|
||||
const visibleTabs = useMemo(() => {
|
||||
if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
|
||||
logger.warn('Invalid tab configuration, resetting to defaults');
|
||||
resetTabConfiguration();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
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: any) => processTab(tab as BaseTabConfig));
|
||||
tabConfiguration.userTabs.forEach((tab: any) => processTab(tab as BaseTabConfig));
|
||||
DEFAULT_TAB_CONFIG.forEach((tab) => processTab(tab as BaseTabConfig));
|
||||
|
||||
return devTabs.sort((a, b) => a.order - b.order);
|
||||
}, [tabConfiguration, 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: Variants = {
|
||||
hidden: { opacity: 0, scale: 0.8 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 20,
|
||||
mass: 0.6,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Reset to default view when modal opens/closes
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
// Reset when closing
|
||||
setActiveTab(null);
|
||||
setLoadingTab(null);
|
||||
} else {
|
||||
// When opening, set to null to show the main view
|
||||
setActiveTab(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Handle closing
|
||||
const handleClose = () => {
|
||||
setActiveTab(null);
|
||||
setLoadingTab(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Handlers
|
||||
const handleBack = () => {
|
||||
setActiveTab(null);
|
||||
};
|
||||
|
||||
const getTabComponent = (tabId: TabType) => {
|
||||
switch (tabId) {
|
||||
case 'settings':
|
||||
return <SettingsTab />;
|
||||
case 'notifications':
|
||||
return <NotificationsTab />;
|
||||
case 'debug':
|
||||
return <DebugTab />;
|
||||
case 'event-logs':
|
||||
return <EventLogsTab />;
|
||||
case 'task-manager':
|
||||
return <TaskManagerTab />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
||||
switch (tabId) {
|
||||
case 'notifications':
|
||||
return hasUnreadNotifications;
|
||||
case 'debug':
|
||||
return hasActiveWarnings;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusMessage = (tabId: TabType): string => {
|
||||
switch (tabId) {
|
||||
case 'notifications':
|
||||
return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
|
||||
case 'debug': {
|
||||
const warnings = activeIssues.filter((i) => i.type === 'warning').length;
|
||||
const errors = activeIssues.filter((i) => i.type === 'error').length;
|
||||
|
||||
return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabClick = (tabId: TabType) => {
|
||||
setLoadingTab(tabId);
|
||||
setActiveTab(tabId);
|
||||
|
||||
// Acknowledge notifications based on tab
|
||||
switch (tabId) {
|
||||
case 'notifications':
|
||||
markAllAsRead();
|
||||
break;
|
||||
case 'debug':
|
||||
acknowledgeAllIssues();
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear loading state after a delay
|
||||
setTimeout(() => setLoadingTab(null), 500);
|
||||
};
|
||||
|
||||
return (
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[100]">
|
||||
<RadixDialog.Overlay asChild>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/70 dark:bg-black/80 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
|
||||
<RadixDialog.Content
|
||||
aria-describedby={undefined}
|
||||
onEscapeKeyDown={handleClose}
|
||||
onPointerDownOutside={handleClose}
|
||||
className="relative z-[101]"
|
||||
>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
'w-[1200px] h-[90vh]',
|
||||
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
||||
'rounded-2xl shadow-2xl',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'flex flex-col overflow-hidden',
|
||||
'relative',
|
||||
)}
|
||||
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 />
|
||||
</div>
|
||||
<div className="relative z-10 flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-4">
|
||||
{activeTab && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex items-center justify-center size-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
||||
>
|
||||
<div className="i-ph:arrow-left size-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Avatar and Dropdown */}
|
||||
<div className="border-l border-gray-200 dark:border-gray-800 pl-6">
|
||||
<AvatarDropdown onSelectTab={handleTabClick} />
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="flex items-center justify-center size-8 rounded-full bg-transparent hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
|
||||
>
|
||||
<div className="i-ph:x size-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className={classNames(
|
||||
'flex-1',
|
||||
'overflow-y-auto',
|
||||
'hover:overflow-y-auto',
|
||||
'scrollbar scrollbar-w-2',
|
||||
'scrollbar-track-transparent',
|
||||
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
||||
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
||||
'will-change-scroll',
|
||||
'touch-auto',
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
key={activeTab || 'home'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="p-6"
|
||||
>
|
||||
{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"
|
||||
></TabTile>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
</div>
|
||||
</RadixDialog.Portal>
|
||||
</RadixDialog.Root>
|
||||
);
|
||||
};
|
||||
135
app/.client/components/@settings/core/TabTile.tsx
Normal file
135
app/.client/components/@settings/core/TabTile.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import { TAB_ICONS, TAB_LABELS } from '~/.client/components/@settings/core/constants';
|
||||
import type { TabVisibilityConfig } from '~/.client/components/@settings/core/types';
|
||||
|
||||
interface TabTileProps {
|
||||
tab: TabVisibilityConfig;
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
hasUpdate?: boolean;
|
||||
statusMessage?: string;
|
||||
description?: string;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabTile: React.FC<TabTileProps> = ({
|
||||
tab,
|
||||
onClick,
|
||||
isActive,
|
||||
hasUpdate,
|
||||
statusMessage,
|
||||
description,
|
||||
isLoading,
|
||||
className,
|
||||
children,
|
||||
}: TabTileProps) => {
|
||||
return (
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<motion.div
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'relative flex flex-col items-center p-6 rounded-xl',
|
||||
'size-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
|
||||
className={classNames(
|
||||
'relative',
|
||||
'size-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' : '',
|
||||
)}
|
||||
>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
TAB_ICONS[tab.id],
|
||||
'size-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' : '',
|
||||
)}
|
||||
>
|
||||
{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',
|
||||
isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Update Indicator with Tooltip */}
|
||||
{hasUpdate && (
|
||||
<>
|
||||
<div className="absolute top-4 right-4 size-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>
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
};
|
||||
49
app/.client/components/@settings/core/constants.ts
Normal file
49
app/.client/components/@settings/core/constants.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { TabType } from './types';
|
||||
|
||||
export const TAB_ICONS: Record<TabType, string> = {
|
||||
profile: 'i-ph:user-circle-fill',
|
||||
settings: 'i-ph:gear-six-fill',
|
||||
notifications: 'i-ph:bell-fill',
|
||||
debug: 'i-ph:bug-fill',
|
||||
'event-logs': 'i-ph:list-bullets-fill',
|
||||
'task-manager': 'i-ph:chart-line-fill',
|
||||
'tab-management': 'i-ph:squares-four-fill',
|
||||
};
|
||||
|
||||
export const TAB_LABELS: Record<TabType, string> = {
|
||||
profile: 'Profile',
|
||||
settings: 'Settings',
|
||||
notifications: 'Notifications',
|
||||
debug: 'Debug',
|
||||
'event-logs': 'Event Logs',
|
||||
'task-manager': 'Task Manager',
|
||||
'tab-management': 'Tab Management',
|
||||
};
|
||||
|
||||
export const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
profile: 'Manage your profile and account settings',
|
||||
settings: 'Configure application preferences',
|
||||
notifications: 'View and manage your notifications',
|
||||
debug: 'Debug tools and system information',
|
||||
'event-logs': 'View system events and logs',
|
||||
'task-manager': 'Monitor system resources and processes',
|
||||
'tab-management': 'Configure visible tabs and their order',
|
||||
};
|
||||
|
||||
export const DEFAULT_TAB_CONFIG = [
|
||||
{ id: 'notifications', visible: true, window: 'user' as const, order: 5 },
|
||||
{ id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
|
||||
|
||||
{ id: 'settings', visible: false, window: 'user' as const, order: 8 },
|
||||
{ id: 'task-manager', visible: false, window: 'user' as const, order: 9 },
|
||||
|
||||
// User Window Tabs (Hidden, controlled by TaskManagerTab)
|
||||
{ id: 'debug', visible: false, window: 'user' as const, order: 11 },
|
||||
|
||||
// Developer Window Tabs (All visible by default)
|
||||
{ id: 'notifications', visible: true, window: 'developer' as const, order: 5 },
|
||||
{ id: 'event-logs', visible: true, window: 'developer' as const, order: 6 },
|
||||
{ id: 'settings', visible: true, window: 'developer' as const, order: 8 },
|
||||
{ id: 'task-manager', visible: true, window: 'developer' as const, order: 9 },
|
||||
{ id: 'debug', visible: true, window: 'developer' as const, order: 11 },
|
||||
];
|
||||
77
app/.client/components/@settings/core/types.ts
Normal file
77
app/.client/components/@settings/core/types.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences';
|
||||
|
||||
export type TabType =
|
||||
| 'profile'
|
||||
| 'settings'
|
||||
| 'notifications'
|
||||
| 'debug'
|
||||
| 'event-logs'
|
||||
| 'task-manager'
|
||||
| 'tab-management';
|
||||
|
||||
export type WindowType = 'user' | 'developer';
|
||||
|
||||
export interface UserProfile {
|
||||
nickname: any;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
notifications: boolean;
|
||||
password?: string;
|
||||
bio?: string;
|
||||
language: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface TabVisibilityConfig {
|
||||
id: TabType;
|
||||
visible: boolean;
|
||||
window: WindowType;
|
||||
order: number;
|
||||
isExtraDevTab?: boolean;
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
export interface DevTabConfig extends TabVisibilityConfig {
|
||||
window: 'developer';
|
||||
}
|
||||
|
||||
export interface UserTabConfig extends TabVisibilityConfig {
|
||||
window: 'user';
|
||||
}
|
||||
|
||||
export interface TabWindowConfig {
|
||||
userTabs: UserTabConfig[];
|
||||
developerTabs: DevTabConfig[];
|
||||
}
|
||||
|
||||
export const categoryLabels: Record<SettingCategory, string> = {
|
||||
profile: 'Profile & Account',
|
||||
file_sharing: 'File Sharing',
|
||||
connectivity: 'Connectivity',
|
||||
system: 'System',
|
||||
services: 'Services',
|
||||
preferences: 'Preferences',
|
||||
};
|
||||
|
||||
export const categoryIcons: Record<SettingCategory, string> = {
|
||||
profile: 'i-ph:user-circle',
|
||||
file_sharing: 'i-ph:folder-simple',
|
||||
connectivity: 'i-ph:wifi-high',
|
||||
system: 'i-ph:gear',
|
||||
services: 'i-ph:cube',
|
||||
preferences: 'i-ph:sliders',
|
||||
};
|
||||
|
||||
export interface Profile {
|
||||
username?: string;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
preferences?: {
|
||||
notifications?: boolean;
|
||||
theme?: 'light' | 'dark' | 'system';
|
||||
language?: string;
|
||||
timezone?: string;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user