Skip to content

Commit 0efb04d

Browse files
Merge pull request #31 from codinit-dev/main
chore: v1.0.9
2 parents 91c2302 + dbc7dae commit 0efb04d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2170
-257
lines changed

AGENTS.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
# Build/Lint/Test Commands
22

33
## Build Commands
4-
- `npm run build` - Production build with Remix Vite
5-
- `npm run dev` - Development server with hot reload
6-
- `npm run typecheck` - TypeScript type checking
4+
- `pnpm run build` - Production build with Remix Vite
5+
- `pnpm run dev` - Development server with hot reload
6+
- `pnpm run typecheck` - TypeScript type checking
77

88
## Test Commands
9-
- `npm run test` - Run all tests once with Vitest
10-
- `npm run test:watch` - Run tests in watch mode
9+
- `pnpm run test` - Run all tests once with Vitest
10+
- `pnpm run test:watch` - Run tests in watch mode
1111
- `vitest --run path/to/test.spec.ts` - Run single test file
1212

1313
## Lint Commands
14-
- `npm run lint` - ESLint check with caching
15-
- `npm run lint:fix` - Auto-fix ESLint + Prettier formatting
14+
- `pnpm run lint` - ESLint check with caching
15+
- `pnpm run lint:fix` - Auto-fix ESLint + Prettier formatting
1616

1717
# Code Style Guidelines
1818

app/components/@settings/core/ControlPanel.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,11 +442,13 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
442442
</RadixDialog.Overlay>
443443

444444
<RadixDialog.Content
445-
aria-describedby={undefined}
446445
onEscapeKeyDown={handleClose}
447446
onPointerDownOutside={handleClose}
448447
className="relative z-[101]"
449448
>
449+
<RadixDialog.Description className="sr-only">
450+
Application settings and configuration panel
451+
</RadixDialog.Description>
450452
<motion.div
451453
className={classNames(
452454
'w-[1200px] h-[90vh]',
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { motion } from 'framer-motion';
2+
import * as RadixDialog from '@radix-ui/react-dialog';
3+
import { classNames } from '~/utils/classNames';
4+
import type { TabType } from '~/components/@settings/core/types';
5+
import { ControlPanelSidebar } from './components/ControlPanelSidebar';
6+
import { ControlPanelContent } from './components/ControlPanelContent';
7+
import { useControlPanelDialog } from './hooks/useControlPanelDialog';
8+
9+
interface ControlPanelDialogProps {
10+
isOpen: boolean;
11+
onClose: () => void;
12+
initialTab?: TabType;
13+
}
14+
15+
export function ControlPanelDialog({ isOpen, onClose, initialTab = 'settings' }: ControlPanelDialogProps) {
16+
const { activeTab, setActiveTab, visibleTabs } = useControlPanelDialog(initialTab);
17+
18+
return (
19+
<RadixDialog.Root open={isOpen} onOpenChange={onClose}>
20+
<RadixDialog.Portal>
21+
<RadixDialog.Overlay asChild>
22+
<motion.div
23+
className="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm"
24+
initial={{ opacity: 0 }}
25+
animate={{ opacity: 1 }}
26+
exit={{ opacity: 0 }}
27+
transition={{ duration: 0.15 }}
28+
/>
29+
</RadixDialog.Overlay>
30+
31+
<div className="fixed inset-0 flex items-center justify-center z-[9999] modern-scrollbar">
32+
<RadixDialog.Content asChild>
33+
<motion.div
34+
className={classNames(
35+
'w-[90vw] h-[700px] max-w-[1500px] max-h-[85vh]',
36+
'bg-codinit-elements-background-depth-1 border border-codinit-elements-borderColor rounded-xl shadow-2xl',
37+
'flex overflow-hidden focus:outline-none',
38+
)}
39+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
40+
animate={{ opacity: 1, scale: 1, y: 0 }}
41+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
42+
transition={{ duration: 0.2, ease: 'easeOut' }}
43+
>
44+
{/* Close button */}
45+
<RadixDialog.Close asChild>
46+
<button
47+
className={classNames(
48+
'absolute top-2 right-2 z-[10000] flex items-center justify-center',
49+
'w-9 h-9 rounded-lg transition-all duration-200',
50+
'bg-transparent text-codinit-elements-textTertiary',
51+
'hover:bg-codinit-elements-background-depth-2 hover:text-codinit-elements-textPrimary',
52+
'focus:outline-none focus:ring-2 focus:ring-codinit-elements-borderColor',
53+
)}
54+
aria-label="Close settings"
55+
>
56+
<div className="i-heroicons:x-mark-solid w-4 h-4" />
57+
</button>
58+
</RadixDialog.Close>
59+
60+
{/* Sidebar */}
61+
<ControlPanelSidebar activeTab={activeTab} onTabChange={setActiveTab} tabs={visibleTabs} />
62+
63+
{/* Main Content */}
64+
<ControlPanelContent activeTab={activeTab} />
65+
</motion.div>
66+
</RadixDialog.Content>
67+
</div>
68+
</RadixDialog.Portal>
69+
</RadixDialog.Root>
70+
);
71+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { Suspense, lazy } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { classNames } from '~/utils/classNames';
4+
import { TAB_LABELS } from '~/components/@settings/core/constants';
5+
import type { TabType } from '~/components/@settings/core/types';
6+
7+
// Lazy load all tab components
8+
const ProfileTab = lazy(() =>
9+
import('~/components/@settings/tabs/profile/ProfileTab').then((module) => ({ default: module.default })),
10+
);
11+
const SettingsTab = lazy(() =>
12+
import('~/components/@settings/tabs/settings/SettingsTab').then((module) => ({ default: module.default })),
13+
);
14+
const NotificationsTab = lazy(() =>
15+
import('~/components/@settings/tabs/notifications/NotificationsTab').then((module) => ({ default: module.default })),
16+
);
17+
const FeaturesTab = lazy(() =>
18+
import('~/components/@settings/tabs/features/FeaturesTab').then((module) => ({ default: module.default })),
19+
);
20+
const DataTab = lazy(() =>
21+
import('~/components/@settings/tabs/data/DataTab').then((module) => ({ default: module.DataTab })),
22+
);
23+
const CloudProvidersTab = lazy(() =>
24+
import('~/components/@settings/tabs/providers/cloud/CloudProvidersTab').then((module) => ({
25+
default: module.default,
26+
})),
27+
);
28+
const LocalProvidersTab = lazy(() =>
29+
import('~/components/@settings/tabs/providers/local/LocalProvidersTab').then((module) => ({
30+
default: module.default,
31+
})),
32+
);
33+
const ServiceStatusTab = lazy(() =>
34+
import('~/components/@settings/tabs/providers/status/ServiceStatusTab').then((module) => ({
35+
default: module.default,
36+
})),
37+
);
38+
const ConnectionsTab = lazy(() =>
39+
import('~/components/@settings/tabs/connections/ConnectionsTab').then((module) => ({ default: module.default })),
40+
);
41+
const DebugTab = lazy(() =>
42+
import('~/components/@settings/tabs/debug/DebugTab').then((module) => ({ default: module.default })),
43+
);
44+
const EventLogsTab = lazy(() =>
45+
import('~/components/@settings/tabs/event-logs/EventLogsTab').then((module) => ({ default: module.EventLogsTab })),
46+
);
47+
const UpdateTab = lazy(() =>
48+
import('~/components/@settings/tabs/update/UpdateTab').then((module) => ({ default: module.default })),
49+
);
50+
const TaskManagerTab = lazy(() =>
51+
import('~/components/@settings/tabs/task-manager/TaskManagerTab').then((module) => ({ default: module.default })),
52+
);
53+
54+
interface ControlPanelContentProps {
55+
activeTab: TabType;
56+
}
57+
58+
function LoadingFallback() {
59+
return (
60+
<div className="flex items-center justify-center h-full">
61+
<div className="flex items-center gap-3 text-codinit-elements-textSecondary">
62+
<div className="i-svg-spinners:90-ring-with-bg w-5 h-5 animate-spin" />
63+
<span className="text-sm">Loading...</span>
64+
</div>
65+
</div>
66+
);
67+
}
68+
69+
function TabContent({ tab }: { tab: TabType }) {
70+
switch (tab) {
71+
case 'profile':
72+
return <ProfileTab />;
73+
case 'settings':
74+
return <SettingsTab />;
75+
case 'notifications':
76+
return <NotificationsTab />;
77+
case 'features':
78+
return <FeaturesTab />;
79+
case 'data':
80+
return <DataTab />;
81+
case 'cloud-providers':
82+
return <CloudProvidersTab />;
83+
case 'local-providers':
84+
return <LocalProvidersTab />;
85+
case 'service-status':
86+
return <ServiceStatusTab />;
87+
case 'connection':
88+
return <ConnectionsTab />;
89+
case 'debug':
90+
return <DebugTab />;
91+
case 'event-logs':
92+
return <EventLogsTab />;
93+
case 'update':
94+
return <UpdateTab />;
95+
case 'task-manager':
96+
return <TaskManagerTab />;
97+
default:
98+
return (
99+
<div className="flex items-center justify-center h-full text-codinit-elements-textSecondary">
100+
<div className="text-center">
101+
<div className="i-heroicons:exclamation-triangle w-12 h-12 mx-auto mb-4 opacity-50" />
102+
<p className="text-sm">Tab not found</p>
103+
</div>
104+
</div>
105+
);
106+
}
107+
}
108+
109+
export function ControlPanelContent({ activeTab }: ControlPanelContentProps) {
110+
return (
111+
<div
112+
className={classNames(
113+
'flex-1 pb-10 relative overflow-y-auto flex flex-col',
114+
'bg-codinit-elements-background-depth-1 px-8',
115+
)}
116+
>
117+
{/* Header */}
118+
<div className="bg-codinit-elements-background-depth-1 sticky top-0 z-layer-3 pt-12 mb-4">
119+
<div className="min-h-9 flex flex-col gap-2">
120+
<h2 className="text-lg font-semibold truncate text-codinit-elements-textPrimary">{TAB_LABELS[activeTab]}</h2>
121+
</div>
122+
</div>
123+
124+
{/* Content */}
125+
<div className="flex flex-col gap-6 flex-1">
126+
<AnimatePresence mode="wait">
127+
<motion.div
128+
key={activeTab}
129+
initial={{ opacity: 0, y: 10 }}
130+
animate={{ opacity: 1, y: 0 }}
131+
exit={{ opacity: 0, y: -10 }}
132+
transition={{ duration: 0.2, ease: 'easeOut' }}
133+
className="flex-1"
134+
>
135+
<Suspense fallback={<LoadingFallback />}>
136+
<TabContent tab={activeTab} />
137+
</Suspense>
138+
</motion.div>
139+
</AnimatePresence>
140+
</div>
141+
</div>
142+
);
143+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { classNames } from '~/utils/classNames';
2+
import { TAB_ICONS, TAB_LABELS } from '~/components/@settings/core/constants';
3+
import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types';
4+
5+
interface ControlPanelSidebarProps {
6+
activeTab: TabType;
7+
onTabChange: (tab: TabType) => void;
8+
tabs: TabVisibilityConfig[];
9+
}
10+
11+
export function ControlPanelSidebar({ activeTab, onTabChange, tabs }: ControlPanelSidebarProps) {
12+
// Group tabs into primary and secondary sections
13+
const primaryTabs = tabs.slice(0, 8); // First 8 tabs as primary
14+
const secondaryTabs = tabs.slice(8); // Rest as secondary
15+
16+
const renderTabButton = (tab: TabVisibilityConfig) => {
17+
const isActive = activeTab === tab.id;
18+
const iconClass = TAB_ICONS[tab.id];
19+
const label = TAB_LABELS[tab.id];
20+
21+
return (
22+
<li key={tab.id} className="first:mt-0">
23+
<button
24+
id={`settings-item-${tab.id}`}
25+
onClick={() => onTabChange(tab.id)}
26+
className={classNames(
27+
'w-full flex items-center shrink-0 justify-center md:justify-start gap-3 h-[33px] px-2.5 rounded-md text-sm font-medium',
28+
'focus-visible:outline-2 focus-visible:outline-codinit-elements-borderColor',
29+
'transition-colors duration-200',
30+
isActive
31+
? 'bg-codinit-elements-item-backgroundActive text-codinit-elements-textPrimary'
32+
: 'bg-transparent hover:bg-codinit-elements-background-depth-3 text-codinit-elements-textTertiary hover:text-codinit-elements-textPrimary',
33+
)}
34+
>
35+
<span className={classNames('shrink-0 text-base', iconClass)} />
36+
<span className="truncate hidden md:block">{label}</span>
37+
</button>
38+
</li>
39+
);
40+
};
41+
42+
return (
43+
<aside
44+
className={classNames(
45+
'h-full overflow-y-auto flex flex-col',
46+
'max-w-80 w-12 md:w-auto md:min-w-1/8',
47+
'bg-codinit-elements-background-depth-2 border-r border-codinit-elements-borderColor',
48+
'p-2 md:p-4 pt-0 md:pt-0',
49+
)}
50+
>
51+
<div className="flex-1 flex flex-col gap-4">
52+
{/* Primary Settings Section */}
53+
{primaryTabs.length > 0 && (
54+
<section className="flex flex-col">
55+
<div className="sticky top-0 bg-codinit-elements-background-depth-2 z-layer-1 text-codinit-elements-textTertiary text-xs font-semibold hidden md:block p-2 md:p-4">
56+
Settings
57+
</div>
58+
<ul className="flex flex-col gap-1">{primaryTabs.map(renderTabButton)}</ul>
59+
</section>
60+
)}
61+
62+
{/* Secondary Settings Section */}
63+
{secondaryTabs.length > 0 && (
64+
<section className="flex flex-col">
65+
<div className="sticky top-0 bg-codinit-elements-background-depth-2 z-layer-1 text-codinit-elements-textTertiary text-xs font-semibold hidden md:block p-2 md:p-4">
66+
Advanced
67+
</div>
68+
<ul className="flex flex-col gap-1">{secondaryTabs.map(renderTabButton)}</ul>
69+
</section>
70+
)}
71+
</div>
72+
</aside>
73+
);
74+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useState, useEffect, useMemo } from 'react';
2+
import { useStore } from '@nanostores/react';
3+
import { tabConfigurationStore, developerModeStore } from '~/lib/stores/settings';
4+
import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types';
5+
6+
export function useControlPanelDialog(initialTab: TabType = 'settings') {
7+
const [activeTab, setActiveTab] = useState<TabType>(initialTab);
8+
const tabConfiguration = useStore(tabConfigurationStore);
9+
const isDeveloperMode = useStore(developerModeStore);
10+
11+
// Get visible tabs based on current configuration
12+
const visibleTabs = useMemo(() => {
13+
const currentWindow = isDeveloperMode ? 'developer' : 'user';
14+
const tabsArray = currentWindow === 'developer' ? tabConfiguration.developerTabs : tabConfiguration.userTabs;
15+
16+
return tabsArray
17+
.filter((tab: TabVisibilityConfig) => tab.visible)
18+
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => a.order - b.order);
19+
}, [tabConfiguration, isDeveloperMode]);
20+
21+
// Ensure active tab is valid when configuration changes
22+
useEffect(() => {
23+
if (!visibleTabs.find((tab: TabVisibilityConfig) => tab.id === activeTab)) {
24+
const firstVisibleTab = visibleTabs[0];
25+
26+
if (firstVisibleTab) {
27+
setActiveTab(firstVisibleTab.id);
28+
}
29+
}
30+
}, [visibleTabs, activeTab]);
31+
32+
// Reset to initial tab when dialog opens
33+
34+
useEffect(() => {
35+
if (visibleTabs.find((tab: TabVisibilityConfig) => tab.id === initialTab)) {
36+
setActiveTab(initialTab);
37+
}
38+
}, [initialTab, visibleTabs]);
39+
40+
return {
41+
activeTab,
42+
setActiveTab,
43+
visibleTabs,
44+
isDeveloperMode,
45+
};
46+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ControlPanelDialog } from './ControlPanelDialog';

app/components/@settings/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Core exports
22
export { ControlPanel } from './core/ControlPanel';
3+
export { ControlPanelDialog } from './core/ControlPanelDialog';
34
export type { TabType, TabVisibilityConfig } from './core/types';
45

56
// Constants

0 commit comments

Comments
 (0)