Workspaces in Workspaces all around

This commit is contained in:
2026-02-19 01:49:15 +00:00
parent 72bca6cd42
commit dd8ab84df5
84 changed files with 3047 additions and 749 deletions
@@ -1,106 +1,52 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { Search, User, Lock, Globe, ListChecks } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
import { Card } from '@/components/Card';
import { useTranslation } from '@/lib/i18n';
import { useMemo } from 'react';
import { User, Lock, Globe, ListChecks } from 'lucide-react';
import type { LayoutNode, PanelComponents } from '@/components/Workspace';
import { WorkspaceLayout } from '@/components/Workspace';
import { appRegistry } from '../../Workspaces/app-registry';
import { createSettingsPanelComponents, type SettingsSection } from '../SettingsPanel';
import { UserData } from './UserData';
import { ChangePassword } from './ChangePassword';
import { Languages } from './Languages';
import { TaskDefaults } from './TaskDefaults';
export const ProfileSettings = () => {
const { t } = useTranslation();
const [search, setSearch] = useState('');
const [expanded, setExpanded] = useState<string[]>([]);
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
const GLOBAL_KEY = 'PROFILE_SETTINGS_SELECTED';
const sections = useMemo(
() => [
{
key: 'profile',
icon: User,
title: t('settings.profile.sections.profileTitle'),
description: t('settings.profile.sections.profileDescription'),
content: <UserData />,
},
{
key: 'change-password',
icon: Lock,
title: t('settings.profile.sections.changePasswordTitle'),
description: t('settings.profile.sections.changePasswordDescription'),
content: <ChangePassword />,
},
{
key: 'tasks',
icon: ListChecks,
title: t('settings.profile.sections.tasksTitle'),
description: t('settings.profile.sections.tasksDescription'),
content: <TaskDefaults />,
},
{
key: 'languages',
icon: Globe,
title: t('settings.profile.sections.languagesTitle'),
description: t('settings.profile.sections.languagesDescription'),
content: <Languages />,
},
],
[t],
const sections: SettingsSection[] = [
{ key: 'profile', icon: User, title: 'Profile', description: 'Avatar, name, and email', content: <UserData /> },
{ key: 'change-password', icon: Lock, title: 'Change Password', description: 'Update your password', content: <ChangePassword /> },
{ key: 'tasks', icon: ListChecks, title: 'Task Defaults', description: 'Default model for tasks', content: <TaskDefaults /> },
{ key: 'languages', icon: Globe, title: 'Languages', description: 'Spoken, default, and translation', content: <Languages /> },
];
const { Sidebar, Content } = createSettingsPanelComponents({
globalKey: GLOBAL_KEY,
sidebarIcon: User,
sidebarLabel: 'Profile',
sections,
});
const layout: LayoutNode = {
type: 'group',
id: 'profile-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'profile-left', appType: null }, size: 20 },
{ node: { type: 'panel', id: 'profile-right', appType: null }, size: 80 },
],
};
export const ProfileSettings = () => {
const panelComponents: PanelComponents = useMemo(
() => ({
'profile-left': Sidebar,
'profile-right': Content,
}),
[],
);
const allKeys = useMemo(() => sections.map((s) => s.key), [sections]);
const matchingKeys = useMemo(() => {
if (!search) return allKeys;
const query = search.toLowerCase();
return sections
.filter((s) => {
const el = sectionRefs.current[s.key];
return (el?.textContent?.toLowerCase() ?? '').includes(query);
})
.map((s) => s.key);
}, [search, allKeys, sections]);
useEffect(() => {
if (search) setExpanded(matchingKeys);
}, [search, matchingKeys]);
return (
<div className="flex justify-center h-full px-4 py-8 overflow-y-auto">
<Card className="w-full max-w-2xl h-fit p-6">
<div className="relative mb-6">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-duck-dark/40" />
<Input
placeholder={t('settings.profile.searchPlaceholder')}
value={search}
onChange={(ev) => setSearch(ev.target.value)}
className="pl-9"
/>
</div>
<Accordion type="multiple" value={expanded} onValueChange={setExpanded}>
{sections.map((section) => (
<div
key={section.key}
ref={(el) => {
sectionRefs.current[section.key] = el;
}}
className={search && !matchingKeys.includes(section.key) ? 'hidden' : ''}
>
<AccordionItem value={section.key}>
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-3">
<section.icon className="h-5 w-5 text-duck-forest shrink-0" />
<div className="text-base font-bold text-duck-dark">{section.title}</div>
</div>
</AccordionTrigger>
<AccordionContent className="px-1">{section.content}</AccordionContent>
</AccordionItem>
</div>
))}
</Accordion>
</Card>
<div className="h-full w-full pt-2">
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} registry={appRegistry} components={panelComponents} />
</div>
);
};