Improve breadcrumbs, headers and tabs (#249)

This commit is contained in:
Hakan Shehu
2025-11-11 09:49:16 -08:00
committed by GitHub
parent 3e4c8b8125
commit 239b4d0632
106 changed files with 961 additions and 1076 deletions

View File

@@ -0,0 +1,62 @@
import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
export const AccountLogoutContainer = () => {
const workspace = useWorkspace();
const navigate = useNavigate();
const { mutate, isPending } = useMutation();
return (
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Logout</h2>
<Separator className="mt-3" />
</div>
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
<div className="flex-1 space-y-2">
<h3 className="font-semibold">Sign out of your account</h3>
<p className="text-sm text-muted-foreground">
All your data will be removed from this device. If there are
pending changes, they will be lost. If you login again, all the
data will be re-synced.
</p>
</div>
<div className="w-full md:w-auto md:shrink-0">
<Button
variant="destructive"
disabled={isPending}
className="w-full cursor-pointer md:w-20"
onClick={async () => {
mutate({
input: {
type: 'account.logout',
accountId: workspace.accountId,
},
onSuccess() {
navigate({
to: '/',
replace: true,
});
},
onError(error) {
toast.error(error.message);
},
});
}}
>
{isPending && <Spinner className="mr-1" />}
Logout
</Button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,8 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AccountLogoutHeader = () => {
return (
<BreadcrumbItem id="logout" avatar={defaultIcons.logout} name="Logout" />
);
};

View File

@@ -1,73 +0,0 @@
import { useNavigate } from '@tanstack/react-router';
import { LogOut } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
export const AccountLogoutScreen = () => {
const workspace = useWorkspace();
const navigate = useNavigate();
const { mutate, isPending } = useMutation();
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <LogOut className={className} />}
name="Logout"
/>
</Breadcrumb>
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Logout</h2>
<Separator className="mt-3" />
</div>
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
<div className="flex-1 space-y-2">
<h3 className="font-semibold">Sign out of your account</h3>
<p className="text-sm text-muted-foreground">
All your data will be removed from this device. If there are
pending changes, they will be lost. If you login again, all the
data will be re-synced.
</p>
</div>
<div className="w-full md:w-auto md:shrink-0">
<Button
variant="destructive"
disabled={isPending}
className="w-full cursor-pointer md:w-20"
onClick={async () => {
mutate({
input: {
type: 'account.logout',
accountId: workspace.accountId,
},
onSuccess() {
navigate({
to: '/',
replace: true,
});
},
onError(error) {
toast.error(error.message);
},
});
}}
>
{isPending && <Spinner className="mr-1" />}
Logout
</Button>
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,6 @@
import { LogOut } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AccountLogoutTab = () => {
return (
<div className="flex items-center space-x-2">
<LogOut className="size-4" />
<span>Logout</span>
</div>
);
return <TabItem id="logout" avatar={defaultIcons.logout} name="Logout" />;
};

View File

@@ -0,0 +1,25 @@
import { AccountDelete } from '@colanode/ui/components/accounts/account-delete';
import { AccountUpdate } from '@colanode/ui/components/accounts/account-update';
import { Separator } from '@colanode/ui/components/ui/separator';
export const AccountSettingsContainer = () => {
return (
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<AccountUpdate />
</div>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Danger Zone</h2>
<Separator className="mt-3" />
</div>
<AccountDelete />
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AccountSettingsHeader = () => {
return (
<BreadcrumbItem
id="settings"
avatar={defaultIcons.settings}
name="Account Settings"
/>
);
};

View File

@@ -1,39 +0,0 @@
import { Settings } from 'lucide-react';
import { AccountDelete } from '@colanode/ui/components/accounts/account-delete';
import { AccountUpdate } from '@colanode/ui/components/accounts/account-update';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { Separator } from '@colanode/ui/components/ui/separator';
export const AccountSettingsScreen = () => {
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Settings className={className} />}
name="Settings"
/>
</Breadcrumb>
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<AccountUpdate />
</div>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<AccountDelete />
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,12 @@
import { Settings } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AccountSettingsTab = () => {
return (
<div className="flex items-center space-x-2">
<Settings className="size-4" />
<span>Account Settings</span>
</div>
<TabItem
id="settings"
avatar={defaultIcons.settings}
name="Account Settings"
/>
);
};

View File

@@ -1,6 +1,6 @@
import { LoginForm } from '@colanode/ui/components/accounts/login-form';
export const LoginScreen = () => {
export const Login = () => {
return (
<div className="grid h-screen min-h-screen w-full grid-cols-1 lg:grid-cols-5">
<div className="items-center justify-center bg-foreground hidden lg:flex lg:col-span-2">

View File

@@ -0,0 +1,134 @@
import { Check, Laptop, Moon, Sun } from 'lucide-react';
import { ThemeColor, ThemeMode } from '@colanode/client/types';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useMetadata } from '@colanode/ui/hooks/use-metadata';
import { cn } from '@colanode/ui/lib/utils';
interface ThemeModeOption {
key: string;
value: ThemeMode | null;
label: string;
icon: typeof Laptop;
title: string;
}
const themeModeOptions: ThemeModeOption[] = [
{
key: 'system',
value: null,
label: 'System',
icon: Laptop,
title: 'Follow system',
},
{
key: 'light',
value: 'light',
label: 'Light',
icon: Sun,
title: 'Light theme',
},
{
key: 'dark',
value: 'dark',
label: 'Dark',
icon: Moon,
title: 'Dark theme',
},
];
const themeColorOptions = [
{ value: 'default', label: 'Default', color: 'oklch(0.205 0 0)' },
{ value: 'blue', label: 'Blue', color: 'oklch(0.623 0.214 259.815)' },
{ value: 'red', label: 'Red', color: 'oklch(0.637 0.237 25.331)' },
{ value: 'rose', label: 'Rose', color: 'oklch(0.645 0.246 16.439)' },
{ value: 'orange', label: 'Orange', color: 'oklch(0.705 0.213 47.604)' },
{ value: 'green', label: 'Green', color: 'oklch(0.723 0.219 149.579)' },
{ value: 'yellow', label: 'Yellow', color: 'oklch(0.795 0.184 86.047)' },
{ value: 'violet', label: 'Violet', color: 'oklch(0.606 0.25 292.717)' },
];
export const AppAppearanceSettingsContainer = () => {
const [themeMode, setThemeMode] = useMetadata('app', 'theme.mode');
const [themeColor, setThemeColor] = useMetadata('app', 'theme.color');
return (
<div className="max-w-4xl space-y-8">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Appearance</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{themeModeOptions.map((option) => {
const isActive =
option.value === null ? !themeMode : themeMode === option.value;
const Icon = option.icon;
return (
<Button
key={option.key}
variant="outline"
onClick={() => {
setThemeMode(option.value ?? undefined);
}}
className={cn(
'h-10 w-full justify-start gap-2 relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.title}
>
<Icon className="size-5" />
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
<div>
<h2 className="text-2xl font-semibold tracking-tight">Color</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 max-w-2xl">
{themeColorOptions.map((option) => {
const isDefault = option.value === 'default';
const isActive = isDefault
? !themeColor
: themeColor === option.value;
return (
<Button
key={option.value}
variant="outline"
onClick={() => {
if (isDefault) {
setThemeColor(undefined);
} else {
setThemeColor(option.value as ThemeColor);
}
}}
className={cn(
'h-10 justify-start gap-3 text-left relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.label}
>
<div
className="size-5 rounded-full border border-border/50 shrink-0"
style={{ backgroundColor: option.color }}
/>
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AppAppearanceSettingsHeader = () => {
return (
<BreadcrumbItem
id="appearance"
avatar={defaultIcons.appearance}
name="Appearance"
/>
);
};

View File

@@ -1,144 +0,0 @@
import { Check, Laptop, Moon, Palette, Sun } from 'lucide-react';
import { ThemeColor, ThemeMode } from '@colanode/client/types';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { useMetadata } from '@colanode/ui/hooks/use-metadata';
import { cn } from '@colanode/ui/lib/utils';
interface ThemeModeOption {
key: string;
value: ThemeMode | null;
label: string;
icon: typeof Laptop;
title: string;
}
const themeModeOptions: ThemeModeOption[] = [
{
key: 'system',
value: null,
label: 'System',
icon: Laptop,
title: 'Follow system',
},
{
key: 'light',
value: 'light',
label: 'Light',
icon: Sun,
title: 'Light theme',
},
{
key: 'dark',
value: 'dark',
label: 'Dark',
icon: Moon,
title: 'Dark theme',
},
];
const themeColorOptions = [
{ value: 'default', label: 'Default', color: 'oklch(0.205 0 0)' },
{ value: 'blue', label: 'Blue', color: 'oklch(0.623 0.214 259.815)' },
{ value: 'red', label: 'Red', color: 'oklch(0.637 0.237 25.331)' },
{ value: 'rose', label: 'Rose', color: 'oklch(0.645 0.246 16.439)' },
{ value: 'orange', label: 'Orange', color: 'oklch(0.705 0.213 47.604)' },
{ value: 'green', label: 'Green', color: 'oklch(0.723 0.219 149.579)' },
{ value: 'yellow', label: 'Yellow', color: 'oklch(0.795 0.184 86.047)' },
{ value: 'violet', label: 'Violet', color: 'oklch(0.606 0.25 292.717)' },
];
export const AppAppearanceSettingsScreen = () => {
const [themeMode, setThemeMode] = useMetadata('app', 'theme.mode');
const [themeColor, setThemeColor] = useMetadata('app', 'theme.color');
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Palette className={className} />}
name="Appearance"
/>
</Breadcrumb>
<div className="max-w-4xl space-y-8">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Appearance</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{themeModeOptions.map((option) => {
const isActive =
option.value === null ? !themeMode : themeMode === option.value;
const Icon = option.icon;
return (
<Button
key={option.key}
variant="outline"
onClick={() => {
setThemeMode(option.value ?? undefined);
}}
className={cn(
'h-10 w-full justify-start gap-2 relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.title}
>
<Icon className="size-5" />
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
<div>
<h2 className="text-2xl font-semibold tracking-tight">Color</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 max-w-2xl">
{themeColorOptions.map((option) => {
const isDefault = option.value === 'default';
const isActive = isDefault
? !themeColor
: themeColor === option.value;
return (
<Button
key={option.value}
variant="outline"
onClick={() => {
if (isDefault) {
setThemeColor(undefined);
} else {
setThemeColor(option.value as ThemeColor);
}
}}
className={cn(
'h-10 justify-start gap-3 text-left relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.label}
>
<div
className="size-5 rounded-full border border-border/50 shrink-0"
style={{ backgroundColor: option.color }}
/>
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,12 @@
import { Palette } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AppAppearanceSettingsTab = () => {
return (
<div className="flex items-center space-x-2">
<Palette className="size-4" />
<span>Appearance</span>
</div>
<TabItem
id="appearance"
avatar={defaultIcons.appearance}
name="Appearance"
/>
);
};

View File

@@ -1,7 +1,7 @@
import { DelayedComponent } from '@colanode/ui/components/ui/delayed-component';
import { Spinner } from '@colanode/ui/components/ui/spinner';
export const AppLoadingScreen = () => {
export const AppLoading = () => {
return (
<div className="min-w-screen flex h-full min-h-screen w-full items-center justify-center">
<DelayedComponent>

View File

@@ -5,8 +5,8 @@ import { build } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { AppAssets } from '@colanode/ui/components/app/app-assets';
import { AppLayout } from '@colanode/ui/components/app/app-layout';
import { AppLoadingScreen } from '@colanode/ui/components/app/app-loading-screen';
import { AppResetScreen } from '@colanode/ui/components/app/app-reset-screen';
import { AppLoading } from '@colanode/ui/components/app/app-loading';
import { AppReset } from '@colanode/ui/components/app/app-reset';
import { AppThemeProvider } from '@colanode/ui/components/app/app-theme-provider';
import { RadarProvider } from '@colanode/ui/components/app/radar-provider';
import { AppContext } from '@colanode/ui/contexts/app';
@@ -43,8 +43,8 @@ export const AppProvider = ({ type }: AppProviderProps) => {
<AppContext.Provider value={{ type }}>
<AppThemeProvider init={initOutput}>
<AppAssets />
{initOutput === null && <AppLoadingScreen />}
{initOutput === 'reset' && <AppResetScreen />}
{initOutput === null && <AppLoading />}
{initOutput === 'reset' && <AppReset />}
{initOutput === 'success' && (
<RadarProvider>
<AppLayout type={type} />

View File

@@ -4,7 +4,7 @@ import { toast } from 'sonner';
import { Button } from '@colanode/ui/components/ui/button';
export const AppResetScreen = () => {
export const AppReset = () => {
const mutation = useMutation({
mutationFn: () => {
return window.colanode.reset();

View File

@@ -1,6 +1,5 @@
import { LocalChannelNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
interface ChannelBreadcrumbItemProps {
channel: LocalChannelNode;
@@ -11,14 +10,8 @@ export const ChannelBreadcrumbItem = ({
}: ChannelBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={channel.id}
name={channel.attributes.name}
avatar={channel.attributes.avatar}
className={className}
/>
)}
id={channel.id}
avatar={channel.attributes.avatar}
name={channel.attributes.name}
/>
);

View File

@@ -1,10 +1,6 @@
import { LocalChannelNode } from '@colanode/client/types';
import { ChannelNotFound } from '@colanode/ui/components/channels/channel-not-found';
import { ChannelSettings } from '@colanode/ui/components/channels/channel-settings';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { Conversation } from '@colanode/ui/components/messages/conversation';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -28,18 +24,10 @@ export const ChannelContainer = ({ channelId }: ChannelContainerProps) => {
const { node: channel, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<ChannelSettings channel={channel} role={role} />
</ContainerSettings>
<Conversation
conversationId={channel.id}
rootId={channel.rootId}
role={role}
/>
</>
<Conversation
conversationId={channel.id}
rootId={channel.rootId}
role={role}
/>
);
};

View File

@@ -2,8 +2,7 @@ import { eq, useLiveQuery } from '@tanstack/react-db';
import { LocalChatNode } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
interface ChatBreadcrumbItemProps {
@@ -13,17 +12,17 @@ interface ChatBreadcrumbItemProps {
export const ChatBreadcrumbItem = ({ chat }: ChatBreadcrumbItemProps) => {
const workspace = useWorkspace();
const userId =
const collaboratorId =
chat && chat.type === 'chat'
? (Object.keys(chat.attributes.collaborators).find(
(id) => id !== workspace.userId
) ?? '')
: '';
const userQuery = useLiveQuery((q) =>
const collaboratorQuery = useLiveQuery((q) =>
q
.from({ users: collections.workspace(workspace.userId).users })
.where(({ users }) => eq(users.id, userId))
.where(({ users }) => eq(users.id, collaboratorId))
.select(({ users }) => ({
id: users.id,
name: users.name,
@@ -32,22 +31,16 @@ export const ChatBreadcrumbItem = ({ chat }: ChatBreadcrumbItemProps) => {
.findOne()
);
const user = userQuery.data;
if (!user) {
const collaborator = collaboratorQuery.data;
if (!collaborator) {
return null;
}
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={user.id}
name={user.name}
avatar={user.avatar}
className={className}
/>
)}
name={user.name}
id={collaborator.id}
avatar={collaborator.avatar}
name={collaborator.name}
/>
);
};

View File

@@ -1,10 +1,6 @@
import { LocalChatNode } from '@colanode/client/types';
import { ChatNotFound } from '@colanode/ui/components/chats/chat-not-found';
import { NodeCollaboratorsPopover } from '@colanode/ui/components/collaborators/node-collaborators-popover';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { Conversation } from '@colanode/ui/components/messages/conversation';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -28,14 +24,6 @@ export const ChatContainer = ({ chatId }: ChatContainerProps) => {
const { node, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<NodeCollaboratorsPopover node={node} nodes={[node]} role={role} />
</ContainerSettings>
<Conversation conversationId={node.id} rootId={node.rootId} role={role} />
</>
<Conversation conversationId={node.id} rootId={node.rootId} role={role} />
);
};

View File

@@ -0,0 +1,11 @@
import { LocalChatNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { NodeCollaboratorsPopover } from '@colanode/ui/components/collaborators/node-collaborators-popover';
interface ChatSettingsProps {
chat: LocalChatNode;
role: NodeRole;
}
export const ChatSettings = ({ chat, role }: ChatSettingsProps) => {
return <NodeCollaboratorsPopover node={chat} nodes={[chat]} role={role} />;
};

View File

@@ -24,7 +24,7 @@ export const NodeCollaboratorsPopover = ({
<PopoverTrigger asChild>
<UserRoundPlus className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
</PopoverTrigger>
<PopoverContent className="mr-2 max-h-128 w-128 overflow-auto">
<PopoverContent className="mr-2 max-h-128 w-lg overflow-auto">
<NodeCollaborators node={node} nodes={nodes} role={role} />
</PopoverContent>
</Popover>

View File

@@ -1,6 +1,5 @@
import { LocalDatabaseNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
interface DatabaseBreadcrumbItemProps {
database: LocalDatabaseNode;
@@ -11,14 +10,8 @@ export const DatabaseBreadcrumbItem = ({
}: DatabaseBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={database.id}
name={database.attributes.name}
avatar={database.attributes.avatar}
className={className}
/>
)}
id={database.id}
avatar={database.attributes.avatar}
name={database.attributes.name}
/>
);

View File

@@ -1,11 +1,7 @@
import { LocalDatabaseNode } from '@colanode/client/types';
import { Database } from '@colanode/ui/components/databases/database';
import { DatabaseNotFound } from '@colanode/ui/components/databases/database-not-found';
import { DatabaseSettings } from '@colanode/ui/components/databases/database-settings';
import { DatabaseViews } from '@colanode/ui/components/databases/database-views';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -29,16 +25,8 @@ export const DatabaseContainer = ({ databaseId }: DatabaseContainerProps) => {
const { node: database, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<DatabaseSettings database={database} role={role} />
</ContainerSettings>
<Database database={database} role={role}>
<DatabaseViews />
</Database>
</>
<Database database={database} role={role}>
<DatabaseViews />
</Database>
);
};

View File

@@ -1,23 +1,16 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
interface FileBreadcrumbItemProps {
file: LocalFileNode;
}
export const FileBreadcrumbItem = ({ file }: FileBreadcrumbItemProps) => {
const workspace = useWorkspace();
return (
<BreadcrumbItem
icon={(className) => (
<FileThumbnail
userId={workspace.userId}
file={file}
className={className}
/>
)}
id={file.id}
avatar={defaultIcons.file}
name={file.attributes.name}
/>
);

View File

@@ -1,10 +1,6 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileBody } from '@colanode/ui/components/files/file-body';
import { FileNotFound } from '@colanode/ui/components/files/file-not-found';
import { FileSettings } from '@colanode/ui/components/files/file-settings';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -24,15 +20,5 @@ export const FileContainer = ({ fileId }: FileContainerProps) => {
return <FileNotFound />;
}
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<FileSettings file={data.node} role={data.role} />
</ContainerSettings>
<FileBody file={data.node} />
</>
);
return <FileBody file={data.node} />;
};

View File

@@ -28,7 +28,7 @@ export const FileSettings = ({ file, role }: FileSettingsProps) => {
<Fragment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
<Settings className="size-4 cursor-pointer text-muted-foreground hover:text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="mr-2 w-56">
<DropdownMenuItem className="flex items-center gap-2" disabled>

View File

@@ -1,6 +1,5 @@
import { LocalFolderNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
interface FolderBreadcrumbItemProps {
folder: LocalFolderNode;
@@ -9,14 +8,8 @@ interface FolderBreadcrumbItemProps {
export const FolderBreadcrumbItem = ({ folder }: FolderBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={folder.id}
name={folder.attributes.name}
avatar={folder.attributes.avatar}
className={className}
/>
)}
id={folder.id}
avatar={folder.attributes.avatar}
name={folder.attributes.name}
/>
);

View File

@@ -1,10 +1,6 @@
import { LocalFolderNode } from '@colanode/client/types';
import { FolderBody } from '@colanode/ui/components/folders/folder-body';
import { FolderNotFound } from '@colanode/ui/components/folders/folder-not-found';
import { FolderSettings } from '@colanode/ui/components/folders/folder-settings';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -27,15 +23,5 @@ export const FolderContainer = ({ folderId }: FolderContainerProps) => {
const { node: folder, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<FolderSettings folder={folder} role={role} />
</ContainerSettings>
<FolderBody folder={folder} role={role} />
</>
);
return <FolderBody folder={folder} role={role} />;
};

View File

@@ -0,0 +1,16 @@
import { Avatar } from '@colanode/ui/components/avatars/avatar';
interface BreadcrumbItemProps {
id: string;
name: string;
avatar?: string | null;
}
export const BreadcrumbItem = ({ id, name, avatar }: BreadcrumbItemProps) => {
return (
<div className="text-muted-foreground flex items-center space-x-2 hover:text-foreground cursor-pointer text-sm">
<Avatar id={id} avatar={avatar} name={name} className="size-4" />
<span>{name}</span>
</div>
);
};

View File

@@ -0,0 +1,21 @@
import { useRouter, useLocation } from '@tanstack/react-router';
import { useMemo } from 'react';
export const ContainerHeader = () => {
const router = useRouter();
const location = useLocation();
const headerComponent = useMemo(() => {
const matches = router.matchRoutes(location.href);
for (let i = matches.length - 1; i >= 0; i--) {
const match = matches[i];
if (match?.context && 'header' in match.context) {
return match.context.header;
}
}
return null;
}, [router, location.href]);
return headerComponent;
};

View File

@@ -1,12 +1,13 @@
import { Outlet } from '@tanstack/react-router';
import { useRef, useState } from 'react';
import { useRef } from 'react';
import { ContainerHeader } from '@colanode/ui/components/layouts/containers/container-header';
import { SidebarMobile } from '@colanode/ui/components/layouts/sidebars/sidebar-mobile';
import {
ScrollArea,
ScrollBar,
ScrollViewport,
} from '@colanode/ui/components/ui/scroll-area';
import { SidebarMobile } from '@colanode/ui/components/workspaces/sidebars/sidebar-mobile';
import { useApp } from '@colanode/ui/contexts/app';
import { ContainerContext } from '@colanode/ui/contexts/container';
import { useIsMobile } from '@colanode/ui/hooks/use-is-mobile';
@@ -15,8 +16,6 @@ import { cn } from '@colanode/ui/lib/utils';
export const Container = () => {
const app = useApp();
const isMobile = useIsMobile();
const [settings, setSettings] = useState<React.ReactNode>(null);
const [breadcrumb, setBreadcrumb] = useState<React.ReactNode>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null!);
const scrollViewportRef = useRef<HTMLDivElement>(null!);
@@ -24,10 +23,6 @@ export const Container = () => {
return (
<ContainerContext.Provider
value={{
setSettings,
setBreadcrumb,
resetSettings: () => setSettings(null),
resetBreadcrumb: () => setBreadcrumb(null),
scrollAreaRef,
scrollViewportRef,
}}
@@ -40,8 +35,9 @@ export const Container = () => {
)}
>
{isMobile && <SidebarMobile />}
{breadcrumb && <div className="flex-1">{breadcrumb}</div>}
{settings}
<div className="flex-1">
<ContainerHeader />
</div>
</div>
<ScrollArea ref={scrollAreaRef} className="overflow-hidden h-full">
<ScrollViewport ref={scrollViewportRef} className="h-full">

View File

@@ -1,7 +1,7 @@
import { ChatCreatePopover } from '@colanode/ui/components/chats/chat-create-popover';
import { ChatSidebarItem } from '@colanode/ui/components/chats/chat-sidebar-item';
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
import { Link } from '@colanode/ui/components/ui/link';
import { SidebarHeader } from '@colanode/ui/components/workspaces/sidebars/sidebar-header';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';

View File

@@ -1,7 +1,7 @@
import { Resizable } from 're-resizable';
import { useCallback } from 'react';
import { Sidebar } from '@colanode/ui/components/workspaces/sidebars/sidebar';
import { Sidebar } from '@colanode/ui/components/layouts/sidebars/sidebar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMetadata } from '@colanode/ui/hooks/use-metadata';

View File

@@ -3,9 +3,9 @@ import { LayoutGrid, MessageCircle, Settings } from 'lucide-react';
import { SidebarMenuType, UploadStatus } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { SidebarMenuFooter } from '@colanode/ui/components/workspaces/sidebars/sidebar-menu-footer';
import { SidebarMenuHeader } from '@colanode/ui/components/workspaces/sidebars/sidebar-menu-header';
import { SidebarMenuIcon } from '@colanode/ui/components/workspaces/sidebars/sidebar-menu-icon';
import { SidebarMenuFooter } from '@colanode/ui/components/layouts/sidebars/sidebar-menu-footer';
import { SidebarMenuHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-menu-header';
import { SidebarMenuIcon } from '@colanode/ui/components/layouts/sidebars/sidebar-menu-icon';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';

View File

@@ -3,7 +3,7 @@ import { useLocation } from '@tanstack/react-router';
import { Menu } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Sidebar } from '@colanode/ui/components/workspaces/sidebars/sidebar';
import { Sidebar } from '@colanode/ui/components/layouts/sidebars/sidebar';
import {
Sheet,
SheetContent,

View File

@@ -11,10 +11,10 @@ import {
import { UploadStatus } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
import { SidebarSettingsItem } from '@colanode/ui/components/layouts/sidebars/sidebar-settings-item';
import { Link } from '@colanode/ui/components/ui/link';
import { Separator } from '@colanode/ui/components/ui/separator';
import { SidebarHeader } from '@colanode/ui/components/workspaces/sidebars/sidebar-header';
import { SidebarSettingsItem } from '@colanode/ui/components/workspaces/sidebars/sidebar-settings-item';
import { useApp } from '@colanode/ui/contexts/app';
import { useWorkspace } from '@colanode/ui/contexts/workspace';

View File

@@ -1,6 +1,6 @@
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
import { SpaceCreateButton } from '@colanode/ui/components/spaces/space-create-button';
import { SpaceSidebarItem } from '@colanode/ui/components/spaces/space-sidebar-item';
import { SidebarHeader } from '@colanode/ui/components/workspaces/sidebars/sidebar-header';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';

View File

@@ -1,10 +1,10 @@
import { useState } from 'react';
import { SidebarMenuType } from '@colanode/client/types';
import { SidebarChats } from '@colanode/ui/components/workspaces/sidebars/sidebar-chats';
import { SidebarMenu } from '@colanode/ui/components/workspaces/sidebars/sidebar-menu';
import { SidebarSettings } from '@colanode/ui/components/workspaces/sidebars/sidebar-settings';
import { SidebarSpaces } from '@colanode/ui/components/workspaces/sidebars/sidebar-spaces';
import { SidebarChats } from '@colanode/ui/components/layouts/sidebars/sidebar-chats';
import { SidebarMenu } from '@colanode/ui/components/layouts/sidebars/sidebar-menu';
import { SidebarSettings } from '@colanode/ui/components/layouts/sidebars/sidebar-settings';
import { SidebarSpaces } from '@colanode/ui/components/layouts/sidebars/sidebar-spaces';
import { useApp } from '@colanode/ui/contexts/app';
import { cn } from '@colanode/ui/lib/utils';

View File

@@ -0,0 +1,16 @@
import { Avatar } from '@colanode/ui/components/avatars/avatar';
interface TabItemProps {
id: string;
name: string;
avatar?: string | null;
}
export const TabItem = ({ id, name, avatar }: TabItemProps) => {
return (
<div className="flex items-center space-x-2 cursor-pointer text-sm">
<Avatar id={id} avatar={avatar} name={name} className="size-4" />
<span>{name}</span>
</div>
);
};

View File

@@ -1,7 +1,6 @@
import { MessageCircle } from 'lucide-react';
import { LocalMessageNode } from '@colanode/client/types';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
interface MessageBreadcrumbItemProps {
message: LocalMessageNode;
@@ -11,9 +10,6 @@ export const MessageBreadcrumbItem = ({
message: _,
}: MessageBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => <MessageCircle className={className} />}
name="Message"
/>
<BreadcrumbItem id="message" avatar={defaultIcons.message} name="Message" />
);
};

View File

@@ -1,8 +1,6 @@
import { LocalMessageNode } from '@colanode/client/types';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { Message } from '@colanode/ui/components/messages/message';
import { MessageNotFound } from '@colanode/ui/components/messages/message-not-found';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { ConversationContext } from '@colanode/ui/contexts/conversation';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -25,23 +23,18 @@ export const MessageContainer = ({ messageId }: MessageContainerProps) => {
}
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ConversationContext.Provider
value={{
id: data.node.id,
role: data.role,
rootId: data.node.rootId,
canCreateMessage: true,
onReply: () => {},
onLastMessageIdChange: () => {},
canDeleteMessage: () => false,
}}
>
<Message message={data.node} />
</ConversationContext.Provider>
</>
<ConversationContext.Provider
value={{
id: data.node.id,
role: data.role,
rootId: data.node.rootId,
canCreateMessage: true,
onReply: () => {},
onLastMessageIdChange: () => {},
canDeleteMessage: () => false,
}}
>
<Message message={data.node} />
</ConversationContext.Provider>
);
};

View File

@@ -18,20 +18,17 @@ import {
import { Link } from '@colanode/ui/components/ui/link';
interface NodeBreadcrumbProps {
breadcrumb: LocalNode[];
nodes: LocalNode[];
}
export const NodeBreadcrumb = ({ breadcrumb }: NodeBreadcrumbProps) => {
// Show ellipsis if we have more than 3 nodes (first + last two)
const showEllipsis = breadcrumb.length > 3;
export const NodeBreadcrumb = ({ nodes }: NodeBreadcrumbProps) => {
const showEllipsis = nodes.length > 3;
// Get visible entries: first node + last two entries
const visibleItems = showEllipsis
? [breadcrumb[0], ...breadcrumb.slice(-2)]
: breadcrumb;
const visibleItems = showEllipsis ? [nodes[0], ...nodes.slice(-2)] : nodes;
// Get middle entries for ellipsis (everything except first and last two)
const ellipsisItems = showEllipsis ? breadcrumb.slice(1, -2) : [];
const ellipsisItems = showEllipsis ? nodes.slice(1, -2) : [];
return (
<Breadcrumb className="grow">

View File

@@ -1,4 +1,3 @@
import { useParams } from '@tanstack/react-router';
import { match } from 'ts-pattern';
import { getIdType, IdType } from '@colanode/core';
@@ -12,11 +11,11 @@ import { PageContainer } from '@colanode/ui/components/pages/page-container';
import { RecordContainer } from '@colanode/ui/components/records/record-container';
import { SpaceContainer } from '@colanode/ui/components/spaces/space-container';
export const NodeScreen = () => {
const { nodeId } = useParams({
from: '/workspace/$userId/$nodeId',
});
interface NodeContainerProps {
nodeId: string;
}
export const NodeContainer = ({ nodeId }: NodeContainerProps) => {
return match(getIdType(nodeId))
.with(IdType.Space, () => <SpaceContainer spaceId={nodeId} />)
.with(IdType.Channel, () => <ChannelContainer channelId={nodeId} />)

View File

@@ -0,0 +1,14 @@
import { BadgeAlert } from 'lucide-react';
export const NodeErrorContainer = () => {
return (
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
<BadgeAlert className="size-12 mb-4" />
<h1 className="text-2xl font-semibold tracking-tight">Node error</h1>
<p className="mt-2 text-sm font-medium text-muted-foreground">
The node you are looking for does not exist. It may have been deleted or
your access has been removed.
</p>
</div>
);
};

View File

@@ -1,25 +0,0 @@
import { BadgeAlert } from 'lucide-react';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
export const NodeErrorScreen = () => {
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <BadgeAlert className={className} />}
name="Node error"
/>
</Breadcrumb>
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
<BadgeAlert className="size-12 mb-4" />
<h1 className="text-2xl font-semibold tracking-tight">Node error</h1>
<p className="mt-2 text-sm font-medium text-muted-foreground">
The node you are looking for does not exist. It may have been deleted
or your access has been removed.
</p>
</div>
</>
);
};

View File

@@ -0,0 +1,27 @@
import { LocalNode } from '@colanode/client/types';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { NodeSettings } from '@colanode/ui/components/nodes/node-settings';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
interface NodeHeaderProps {
nodeId: string;
}
export const NodeHeader = ({ nodeId }: NodeHeaderProps) => {
const data = useNodeContainer<LocalNode>(nodeId);
if (data.isPending) {
return null;
}
if (!data.node) {
return null;
}
return (
<div className="flex items-center gap-2">
<NodeBreadcrumb nodes={data.breadcrumb} />
<NodeSettings node={data.node} role={data.role} />
</div>
);
};

View File

@@ -0,0 +1,46 @@
import { LocalNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { ChannelSettings } from '@colanode/ui/components/channels/channel-settings';
import { NodeCollaboratorsPopover } from '@colanode/ui/components/collaborators/node-collaborators-popover';
import { DatabaseSettings } from '@colanode/ui/components/databases/database-settings';
import { FileSettings } from '@colanode/ui/components/files/file-settings';
import { FolderSettings } from '@colanode/ui/components/folders/folder-settings';
import { PageSettings } from '@colanode/ui/components/pages/page-settings';
import { RecordSettings } from '@colanode/ui/components/records/record-settings';
interface NodeSettingsProps {
node: LocalNode;
role: NodeRole;
}
export const NodeSettings = ({ node, role }: NodeSettingsProps) => {
if (node.type === 'channel') {
return <ChannelSettings channel={node} role={role} />;
}
if (node.type === 'chat') {
return <NodeCollaboratorsPopover node={node} nodes={[node]} role={role} />;
}
if (node.type === 'database') {
return <DatabaseSettings database={node} role={role} />;
}
if (node.type === 'folder') {
return <FolderSettings folder={node} role={role} />;
}
if (node.type === 'file') {
return <FileSettings file={node} role={role} />;
}
if (node.type === 'page') {
return <PageSettings page={node} role={role} />;
}
if (node.type === 'record') {
return <RecordSettings record={node} role={role} />;
}
return null;
};

View File

@@ -1,6 +1,5 @@
import { LocalPageNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
interface PageBreadcrumbItemProps {
page: LocalPageNode;
@@ -9,14 +8,8 @@ interface PageBreadcrumbItemProps {
export const PageBreadcrumbItem = ({ page }: PageBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={page.id}
name={page.attributes.name}
avatar={page.attributes.avatar}
className={className}
/>
)}
id={page.id}
avatar={page.attributes.avatar}
name={page.attributes.name}
/>
);

View File

@@ -1,10 +1,6 @@
import { LocalPageNode } from '@colanode/client/types';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { PageBody } from '@colanode/ui/components/pages/page-body';
import { PageNotFound } from '@colanode/ui/components/pages/page-not-found';
import { PageSettings } from '@colanode/ui/components/pages/page-settings';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -26,15 +22,5 @@ export const PageContainer = ({ pageId }: PageContainerProps) => {
const { node: page, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<PageSettings page={page} role={role} />
</ContainerSettings>
<PageBody page={page} role={role} />
</>
);
return <PageBody page={page} role={role} />;
};

View File

@@ -1,6 +1,5 @@
import { LocalRecordNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
interface RecordBreadcrumbItemProps {
record: LocalRecordNode;
@@ -9,14 +8,8 @@ interface RecordBreadcrumbItemProps {
export const RecordBreadcrumbItem = ({ record }: RecordBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={record.id}
name={record.attributes.name}
avatar={record.attributes.avatar}
className={className}
/>
)}
id={record.id}
avatar={record.attributes.avatar}
name={record.attributes.name}
/>
);

View File

@@ -1,10 +1,6 @@
import { LocalRecordNode } from '@colanode/client/types';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { ContainerSettings } from '@colanode/ui/components/workspaces/containers/container-settings';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { RecordBody } from '@colanode/ui/components/records/record-body';
import { RecordNotFound } from '@colanode/ui/components/records/record-not-found';
import { RecordSettings } from '@colanode/ui/components/records/record-settings';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
@@ -27,15 +23,5 @@ export const RecordContainer = ({ recordId }: RecordContainerProps) => {
const { node: record, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<ContainerSettings>
<RecordSettings record={record} role={role} />
</ContainerSettings>
<RecordBody record={record} role={role} />
</>
);
return <RecordBody record={record} role={role} />;
};

View File

@@ -1,6 +1,5 @@
import { LocalSpaceNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
interface SpaceBreadcrumbItemProps {
space: LocalSpaceNode;
@@ -9,14 +8,8 @@ interface SpaceBreadcrumbItemProps {
export const SpaceBreadcrumbItem = ({ space }: SpaceBreadcrumbItemProps) => {
return (
<BreadcrumbItem
icon={(className) => (
<Avatar
id={space.id}
name={space.attributes.name}
avatar={space.attributes.avatar}
className={className}
/>
)}
id={space.id}
avatar={space.attributes.avatar}
name={space.attributes.name}
/>
);

View File

@@ -1,6 +1,4 @@
import { LocalSpaceNode } from '@colanode/client/types';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { SpaceBody } from '@colanode/ui/components/spaces/space-body';
import { SpaceNotFound } from '@colanode/ui/components/spaces/space-not-found';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
@@ -25,12 +23,5 @@ export const SpaceContainer = ({ spaceId }: SpaceContainerProps) => {
const { node, role } = data;
return (
<>
<Breadcrumb>
<NodeBreadcrumb breadcrumb={data.breadcrumb} />
</Breadcrumb>
<SpaceBody space={node} role={role} />
</>
);
return <SpaceBody space={node} role={role} />;
};

View File

@@ -6,6 +6,7 @@ import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { extractNodeRole } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { WorkspaceSidebarItem } from '@colanode/ui/components/layouts/sidebars/sidebar-item';
import { SpaceSidebarDropdown } from '@colanode/ui/components/spaces/space-sidebar-dropdown';
import {
Collapsible,
@@ -13,7 +14,6 @@ import {
CollapsibleTrigger,
} from '@colanode/ui/components/ui/collapsible';
import { Link } from '@colanode/ui/components/ui/link';
import { WorkspaceSidebarItem } from '@colanode/ui/components/workspaces/sidebars/sidebar-item';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';

View File

@@ -13,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
<ol
data-slot="breadcrumb-list"
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-word sm:gap-2.5',
className
)}
{...props}

View File

@@ -1,25 +0,0 @@
import { cn } from '@colanode/ui/lib/utils';
interface BreadcrumbItemProps {
icon: (className: string) => React.ReactNode;
name: string;
className?: string;
}
export const BreadcrumbItem = ({
icon,
name,
className,
}: BreadcrumbItemProps) => {
return (
<div
className={cn(
'text-muted-foreground flex items-center space-x-2 hover:text-foreground cursor-pointer text-sm',
className
)}
>
{icon('size-4')}
<span>{name}</span>
</div>
);
};

View File

@@ -1,21 +0,0 @@
import { useEffect } from 'react';
import { useContainer } from '@colanode/ui/contexts/container';
interface BreadcrumbProps {
children: React.ReactNode;
}
export const Breadcrumb = ({ children }: BreadcrumbProps) => {
const container = useContainer();
useEffect(() => {
container.setBreadcrumb(children);
return () => {
container.resetBreadcrumb();
};
}, [children]);
return null;
};

View File

@@ -1,21 +0,0 @@
import { useEffect } from 'react';
import { useContainer } from '@colanode/ui/contexts/container';
interface ContainerSettingsProps {
children: React.ReactNode;
}
export const ContainerSettings = ({ children }: ContainerSettingsProps) => {
const container = useContainer();
useEffect(() => {
container.setSettings(children);
return () => {
container.resetSettings();
};
}, [children]);
return null;
};

View File

@@ -0,0 +1,35 @@
import { useLiveQuery } from '@tanstack/react-db';
import { collections } from '@colanode/ui/collections';
import { Separator } from '@colanode/ui/components/ui/separator';
import { WorkspaceDownloadFile } from '@colanode/ui/components/workspaces/downloads/workspace-download-file';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
export const WorkspaceDownloadsContainer = () => {
const workspace = useWorkspace();
const downloadsQuery = useLiveQuery((q) =>
q
.from({ downloads: collections.workspace(workspace.userId).downloads })
.select(({ downloads }) => downloads)
.orderBy(({ downloads }) => downloads.id, 'desc')
);
const downloads = downloadsQuery.data ?? [];
return (
<div className="overflow-y-auto">
<div className="max-w-4xl space-y-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Downloads</h2>
<Separator className="mt-3" />
</div>
<div className="space-y-4 w-full">
{downloads.map((download) => (
<WorkspaceDownloadFile key={download.id} download={download} />
))}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceDownloadsHeader = () => {
return (
<BreadcrumbItem
id="downloads"
avatar={defaultIcons.downloads}
name="Downloads"
/>
);
};

View File

@@ -1,46 +0,0 @@
import { useLiveQuery } from '@tanstack/react-db';
import { Download } from 'lucide-react';
import { collections } from '@colanode/ui/collections';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { WorkspaceDownloadFile } from '@colanode/ui/components/workspaces/downloads/workspace-download-file';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
export const WorkspaceDownloadsScreen = () => {
const workspace = useWorkspace();
const downloadsQuery = useLiveQuery((q) =>
q
.from({ downloads: collections.workspace(workspace.userId).downloads })
.select(({ downloads }) => downloads)
.orderBy(({ downloads }) => downloads.id, 'desc')
);
const downloads = downloadsQuery.data ?? [];
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Download className={className} />}
name="Downloads"
/>
</Breadcrumb>
<div className="overflow-y-auto">
<div className="max-w-4xl space-y-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Downloads</h2>
<Separator className="mt-3" />
</div>
<div className="space-y-4 w-full">
{downloads.map((download) => (
<WorkspaceDownloadFile key={download.id} download={download} />
))}
</div>
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,8 @@
import { Download } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceDownloadsTab = () => {
return (
<div className="flex items-center space-x-2">
<Download className="size-4" />
<span>Downloads</span>
</div>
<TabItem id="downloads" avatar={defaultIcons.downloads} name="Downloads" />
);
};

View File

@@ -0,0 +1,16 @@
import { UserStorageStats } from '@colanode/ui/components/workspaces/storage/user-storage-stats';
import { WorkspaceStorageStats } from '@colanode/ui/components/workspaces/storage/workspace-storage-stats';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
export const WorkspaceStorageContainer = () => {
const workspace = useWorkspace();
const canManageStorage =
workspace.role === 'owner' || workspace.role === 'admin';
return (
<div className="max-w-4xl space-y-10">
<UserStorageStats />
{canManageStorage && <WorkspaceStorageStats />}
</div>
);
};

View File

@@ -0,0 +1,8 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceStorageHeader = () => {
return (
<BreadcrumbItem id="storage" avatar={defaultIcons.storage} name="Storage" />
);
};

View File

@@ -1,28 +0,0 @@
import { Cylinder } from 'lucide-react';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { UserStorageStats } from '@colanode/ui/components/workspaces/storage/user-storage-stats';
import { WorkspaceStorageStats } from '@colanode/ui/components/workspaces/storage/workspace-storage-stats';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
export const WorkspaceStorageScreen = () => {
const workspace = useWorkspace();
const canManageStorage =
workspace.role === 'owner' || workspace.role === 'admin';
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Cylinder className={className} />}
name="Storage"
/>
</Breadcrumb>
<div className="max-w-4xl space-y-10">
<UserStorageStats />
{canManageStorage && <WorkspaceStorageStats />}
</div>
</>
);
};

View File

@@ -1,10 +1,6 @@
import { Cylinder } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceStorageTab = () => {
return (
<div className="flex items-center space-x-2">
<Cylinder className="size-4" />
<span>Workspace Storage</span>
</div>
);
return <TabItem id="storage" avatar={defaultIcons.storage} name="Storage" />;
};

View File

@@ -0,0 +1,54 @@
import { useState } from 'react';
import { InView } from 'react-intersection-observer';
import { UploadListQueryInput } from '@colanode/client/queries';
import { Separator } from '@colanode/ui/components/ui/separator';
import { WorkspaceUploadFile } from '@colanode/ui/components/workspaces/uploads/workspace-upload-file';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const UPLOADS_PER_PAGE = 100;
export const WorkspaceUploadsContainer = () => {
const workspace = useWorkspace();
const [lastPage, setLastPage] = useState<number>(1);
const inputs: UploadListQueryInput[] = Array.from({
length: lastPage,
}).map((_, i) => ({
type: 'upload.list',
userId: workspace.userId,
count: UPLOADS_PER_PAGE,
page: i + 1,
}));
const result = useLiveQueries(inputs);
const uploads = result.flatMap((data) => data.data ?? []);
const isPending = result.some((data) => data.isPending);
const hasMore = !isPending && uploads.length === lastPage * UPLOADS_PER_PAGE;
return (
<div className="overflow-y-auto">
<div className="max-w-4xl space-y-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Uploads</h2>
<Separator className="mt-3" />
</div>
<div className="space-y-4 w-full">
{uploads.map((upload) => (
<WorkspaceUploadFile key={upload.fileId} upload={upload} />
))}
</div>
<InView
rootMargin="200px"
onChange={(inView) => {
if (inView && hasMore && !isPending) {
setLastPage(lastPage + 1);
}
}}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,8 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceUploadsHeader = () => {
return (
<BreadcrumbItem id="uploads" avatar={defaultIcons.uploads} name="Uploads" />
);
};

View File

@@ -1,65 +0,0 @@
import { Upload } from 'lucide-react';
import { useState } from 'react';
import { InView } from 'react-intersection-observer';
import { UploadListQueryInput } from '@colanode/client/queries';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { WorkspaceUploadFile } from '@colanode/ui/components/workspaces/uploads/workspace-upload-file';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const UPLOADS_PER_PAGE = 100;
export const WorkspaceUploadsScreen = () => {
const workspace = useWorkspace();
const [lastPage, setLastPage] = useState<number>(1);
const inputs: UploadListQueryInput[] = Array.from({
length: lastPage,
}).map((_, i) => ({
type: 'upload.list',
userId: workspace.userId,
count: UPLOADS_PER_PAGE,
page: i + 1,
}));
const result = useLiveQueries(inputs);
const uploads = result.flatMap((data) => data.data ?? []);
const isPending = result.some((data) => data.isPending);
const hasMore = !isPending && uploads.length === lastPage * UPLOADS_PER_PAGE;
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Upload className={className} />}
name="Uploads"
/>
</Breadcrumb>
<div className="overflow-y-auto">
<div className="max-w-4xl space-y-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Uploads</h2>
<Separator className="mt-3" />
</div>
<div className="space-y-4 w-full">
{uploads.map((upload) => (
<WorkspaceUploadFile key={upload.fileId} upload={upload} />
))}
</div>
<InView
rootMargin="200px"
onChange={(inView) => {
if (inView && hasMore && !isPending) {
setLastPage(lastPage + 1);
}
}}
/>
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,6 @@
import { Upload } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceUploadsTab = () => {
return (
<div className="flex items-center space-x-2">
<Upload className="size-4" />
<span>Uploads</span>
</div>
);
return <TabItem id="uploads" avatar={defaultIcons.uploads} name="Uploads" />;
};

View File

@@ -6,7 +6,7 @@ import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
export const WorkspaceCreateScreen = () => {
export const WorkspaceCreate = () => {
const workspace = useWorkspace();
const router = useRouter();
const { mutate, isPending } = useMutation();

View File

@@ -0,0 +1,12 @@
export const WorkspaceHomeContainer = () => {
return (
<div className="h-full w-full flex flex-col gap-1">
<div className="h-10 app-drag-region"></div>
<div className="grow flex items-center justify-center">
<p className="text-sm text-muted-foreground">
What did you get done this week?
</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,6 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceHomeHeader = () => {
return <BreadcrumbItem id="home" avatar={defaultIcons.home} name="Home" />;
};

View File

@@ -1,25 +0,0 @@
import { Home } from 'lucide-react';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
export const WorkspaceHomeScreen = () => {
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Home className={className} />}
name="Home"
/>
</Breadcrumb>
<div className="h-full w-full flex flex-col gap-1">
<div className="h-10 app-drag-region"></div>
<div className="grow flex items-center justify-center">
<p className="text-sm text-muted-foreground">
What did you get done this week?
</p>
</div>
</div>
</>
);
};

View File

@@ -1,5 +1,5 @@
import { Container } from '@colanode/ui/components/workspaces/containers/container';
import { SidebarDesktop } from '@colanode/ui/components/workspaces/sidebars/sidebar-desktop';
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { SidebarDesktop } from '@colanode/ui/components/layouts/sidebars/sidebar-desktop';
import { useIsMobile } from '@colanode/ui/hooks/use-is-mobile';
export const WorkspaceLayout = () => {

View File

@@ -0,0 +1,80 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { toast } from 'sonner';
import { collections } from '@colanode/ui/collections';
import { Separator } from '@colanode/ui/components/ui/separator';
import { WorkspaceDelete } from '@colanode/ui/components/workspaces/workspace-delete';
import { WorkspaceForm } from '@colanode/ui/components/workspaces/workspace-form';
import { WorkspaceNotFound } from '@colanode/ui/components/workspaces/workspace-not-found';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
export const WorkspaceSettingsContainer = () => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const currentWorkspaceQuery = useLiveQuery((q) =>
q
.from({ workspaces: collections.workspaces })
.where(({ workspaces }) => eq(workspaces.accountId, workspace.accountId))
.select(({ workspaces }) => ({
name: workspaces.name,
description: workspaces.description,
avatar: workspaces.avatar,
}))
);
const currentWorkspace = currentWorkspaceQuery.data?.[0];
const canEdit = workspace.role === 'owner';
if (!currentWorkspace) {
return <WorkspaceNotFound />;
}
return (
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<WorkspaceForm
readOnly={!canEdit}
values={{
name: currentWorkspace.name,
description: currentWorkspace.description ?? '',
avatar: currentWorkspace.avatar ?? null,
}}
onSubmit={(values) => {
mutate({
input: {
type: 'workspace.update',
id: workspace.workspaceId,
userId: workspace.userId,
name: values.name,
description: values.description,
avatar: values.avatar ?? null,
},
onSuccess() {
toast.success('Workspace updated');
},
onError(error) {
toast.error(error.message);
},
});
}}
isSaving={isPending}
saveText="Update"
/>
</div>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Danger Zone</h2>
<Separator className="mt-3" />
</div>
<WorkspaceDelete />
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceSettingsHeader = () => {
return (
<BreadcrumbItem
id="settings"
avatar={defaultIcons.settings}
name="Settings"
/>
);
};

View File

@@ -1,93 +0,0 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { Settings } from 'lucide-react';
import { toast } from 'sonner';
import { collections } from '@colanode/ui/collections';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { WorkspaceDelete } from '@colanode/ui/components/workspaces/workspace-delete';
import { WorkspaceForm } from '@colanode/ui/components/workspaces/workspace-form';
import { WorkspaceNotFound } from '@colanode/ui/components/workspaces/workspace-not-found';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
export const WorkspaceSettingsScreen = () => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const currentWorkspaceQuery = useLiveQuery((q) =>
q
.from({ workspaces: collections.workspaces })
.where(({ workspaces }) => eq(workspaces.accountId, workspace.accountId))
.select(({ workspaces }) => ({
name: workspaces.name,
description: workspaces.description,
avatar: workspaces.avatar,
}))
);
const currentWorkspace = currentWorkspaceQuery.data?.[0];
const canEdit = workspace.role === 'owner';
if (!currentWorkspace) {
return <WorkspaceNotFound />;
}
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Settings className={className} />}
name="Settings"
/>
</Breadcrumb>
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<WorkspaceForm
readOnly={!canEdit}
values={{
name: currentWorkspace.name,
description: currentWorkspace.description ?? '',
avatar: currentWorkspace.avatar ?? null,
}}
onSubmit={(values) => {
mutate({
input: {
type: 'workspace.update',
id: workspace.workspaceId,
userId: workspace.userId,
name: values.name,
description: values.description,
avatar: values.avatar ?? null,
},
onSuccess() {
toast.success('Workspace updated');
},
onError(error) {
toast.error(error.message);
},
});
}}
isSaving={isPending}
saveText="Update"
/>
</div>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<WorkspaceDelete />
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,12 @@
import { Settings } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceSettingsTab = () => {
return (
<div className="flex items-center space-x-2">
<Settings className="size-4" />
<span>Workspace Settings</span>
</div>
<TabItem
id="settings"
avatar={defaultIcons.settings}
name="Workspace Settings"
/>
);
};

View File

@@ -0,0 +1,96 @@
import { useState } from 'react';
import { InView } from 'react-intersection-observer';
import { UserListQueryInput } from '@colanode/client/queries';
import { WorkspaceRole } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { WorkspaceUserInvite } from '@colanode/ui/components/workspaces/workspace-user-invite';
import { WorkspaceUserRoleDropdown } from '@colanode/ui/components/workspaces/workspace-user-role-dropdown';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const USERS_PER_PAGE = 50;
export const WorkspaceUsersContainer = () => {
const workspace = useWorkspace();
const canEditUsers = workspace.role === 'owner' || workspace.role === 'admin';
const [lastPage, setLastPage] = useState<number>(1);
const inputs: UserListQueryInput[] = Array.from({
length: lastPage,
}).map((_, i) => ({
type: 'user.list',
page: i + 1,
count: USERS_PER_PAGE,
userId: workspace.userId,
}));
const result = useLiveQueries(inputs);
const users = result.flatMap((data) => data.data ?? []);
const isPending = result.some((data) => data.isPending);
const hasMore = !isPending && users.length === lastPage * USERS_PER_PAGE;
return (
<div className="max-w-4xl space-y-8">
{canEditUsers && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Invite</h2>
<Separator className="mt-3" />
</div>
<WorkspaceUserInvite />
</div>
)}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Users</h2>
<p className="text-sm text-muted-foreground mt-1">
The list of all users on the workspace
</p>
<Separator className="mt-3" />
</div>
<div className="flex flex-col gap-3">
{users.map((user) => {
const name: string = user.name ?? 'Unknown';
const email: string = user.email ?? ' ';
const avatar: string | null | undefined = user.avatar;
const role: WorkspaceRole = user.role;
if (!role) {
return null;
}
return (
<div key={user.id} className="flex items-center space-x-3">
<Avatar id={user.id} name={name} avatar={avatar} />
<div className="grow">
<p className="text-sm font-medium leading-none">{name}</p>
<p className="text-sm text-muted-foreground">{email}</p>
</div>
<WorkspaceUserRoleDropdown
userId={user.id}
value={role}
canEdit={canEditUsers}
/>
</div>
);
})}
<div className="flex items-center justify-center space-x-3">
{isPending && <Spinner />}
</div>
<InView
rootMargin="200px"
onChange={(inView) => {
if (inView && hasMore && !isPending) {
setLastPage(lastPage + 1);
}
}}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,6 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceUsersHeader = () => {
return <BreadcrumbItem id="users" avatar={defaultIcons.users} name="Users" />;
};

View File

@@ -1,107 +0,0 @@
import { Users } from 'lucide-react';
import { useState } from 'react';
import { InView } from 'react-intersection-observer';
import { UserListQueryInput } from '@colanode/client/queries';
import { WorkspaceRole } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { Breadcrumb } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb';
import { BreadcrumbItem } from '@colanode/ui/components/workspaces/breadcrumbs/breadcrumb-item';
import { WorkspaceUserInvite } from '@colanode/ui/components/workspaces/workspace-user-invite';
import { WorkspaceUserRoleDropdown } from '@colanode/ui/components/workspaces/workspace-user-role-dropdown';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const USERS_PER_PAGE = 50;
export const WorkspaceUsersScreen = () => {
const workspace = useWorkspace();
const canEditUsers = workspace.role === 'owner' || workspace.role === 'admin';
const [lastPage, setLastPage] = useState<number>(1);
const inputs: UserListQueryInput[] = Array.from({
length: lastPage,
}).map((_, i) => ({
type: 'user.list',
page: i + 1,
count: USERS_PER_PAGE,
userId: workspace.userId,
}));
const result = useLiveQueries(inputs);
const users = result.flatMap((data) => data.data ?? []);
const isPending = result.some((data) => data.isPending);
const hasMore = !isPending && users.length === lastPage * USERS_PER_PAGE;
return (
<>
<Breadcrumb>
<BreadcrumbItem
icon={(className) => <Users className={className} />}
name="Users"
/>
</Breadcrumb>
<div className="max-w-4xl space-y-8">
{canEditUsers && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Invite</h2>
<Separator className="mt-3" />
</div>
<WorkspaceUserInvite />
</div>
)}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Users</h2>
<p className="text-sm text-muted-foreground mt-1">
The list of all users on the workspace
</p>
<Separator className="mt-3" />
</div>
<div className="flex flex-col gap-3">
{users.map((user) => {
const name: string = user.name ?? 'Unknown';
const email: string = user.email ?? ' ';
const avatar: string | null | undefined = user.avatar;
const role: WorkspaceRole = user.role;
if (!role) {
return null;
}
return (
<div key={user.id} className="flex items-center space-x-3">
<Avatar id={user.id} name={name} avatar={avatar} />
<div className="grow">
<p className="text-sm font-medium leading-none">{name}</p>
<p className="text-sm text-muted-foreground">{email}</p>
</div>
<WorkspaceUserRoleDropdown
userId={user.id}
value={role}
canEdit={canEditUsers}
/>
</div>
);
})}
<div className="flex items-center justify-center space-x-3">
{isPending && <Spinner />}
</div>
<InView
rootMargin="200px"
onChange={(inView) => {
if (inView && hasMore && !isPending) {
setLastPage(lastPage + 1);
}
}}
/>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,6 @@
import { Users } from 'lucide-react';
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceUsersTab = () => {
return (
<div className="flex items-center space-x-2">
<Users className="size-4" />
<span>Workspace Users</span>
</div>
);
return <TabItem id="users" avatar={defaultIcons.users} name="Users" />;
};

View File

@@ -1,5 +1,4 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { useParams } from '@tanstack/react-router';
import { collections } from '@colanode/ui/collections';
import { ServerProvider } from '@colanode/ui/components/servers/server-provider';
@@ -8,11 +7,11 @@ import { WorkspaceNotFound } from '@colanode/ui/components/workspaces/workspace-
import { WorkspaceContext } from '@colanode/ui/contexts/workspace';
import { useLocationTracker } from '@colanode/ui/hooks/use-location-tracker';
export const WorkspaceScreen = () => {
const { userId } = useParams({
from: '/workspace/$userId',
});
interface WorkspaceProps {
userId: string;
}
export const Workspace = ({ userId }: WorkspaceProps) => {
const workspaceQuery = useLiveQuery((q) =>
q
.from({ workspaces: collections.workspaces })

View File

@@ -1,10 +1,6 @@
import { createContext, useContext } from 'react';
interface ContainerContext {
setSettings: (settings: React.ReactNode) => void;
resetSettings: () => void;
setBreadcrumb: (breadcrumb: React.ReactNode) => void;
resetBreadcrumb: () => void;
scrollAreaRef: React.RefObject<HTMLDivElement>;
scrollViewportRef: React.RefObject<HTMLDivElement>;
}

View File

@@ -12,4 +12,15 @@ export const defaultIcons = {
bookmark: '01jhzfk3g4q40x7927qcm0hrjdic',
folder: '01jhzfk3jrgc276z2gdabm4cwmic',
apps: '01jhzfk4m7djqd1pw0e1671cmric',
logout: '01jhzfk4pv13qxjprqgqfeqp73ic',
settings: '01jhzfk4ra4fvcay6qgrydgsf5ic',
appearance: '01jhzfk39qxa7xtr7z69fyrb2pic',
message: '01jhzfk36869zq5cp6ke5vvzd6ic',
users: '01jhzfk4tvxa8vtxwgm341w2hmic',
home: '01jhzfk2zt07xss9tqfvca1g6eic',
uploads: '01jhzfk4srtdjmd5zk0hd3fjs4ic',
downloads: '01jhzfk4n5app62xvg1m2s3nv7ic',
storage: '01jhzfk3d19d8enym86tat3ybeic',
error: '01jhzfk4naap5xt27cbkj3mbm6ic',
file: '01jhzfk3gkaz5dnfn1ew16b2pcic',
};

View File

@@ -1,13 +1,13 @@
import { createRoute } from '@tanstack/react-router';
import { WorkspaceCreateScreen } from '@colanode/ui/components/workspaces/workspace-create-screen';
import { WorkspaceCreate } from '@colanode/ui/components/workspaces/workspace-create';
import { WorkspaceCreateTab } from '@colanode/ui/components/workspaces/workspace-create-tab';
import { rootRoute } from '@colanode/ui/routes/root';
export const workspaceCreateRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/create',
component: WorkspaceCreateScreen,
component: WorkspaceCreate,
context: () => {
return {
tab: <WorkspaceCreateTab />,

View File

@@ -1,13 +1,13 @@
import { createRoute } from '@tanstack/react-router';
import { LoginScreen } from '@colanode/ui/components/accounts/login-screen';
import { Login } from '@colanode/ui/components/accounts/login';
import { LoginTab } from '@colanode/ui/components/accounts/login-tab';
import { rootRoute } from '@colanode/ui/routes/root';
export const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/login',
component: LoginScreen,
component: Login,
context: () => {
return {
tab: <LoginTab />,

View File

@@ -1,6 +1,7 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { AccountLogoutScreen } from '@colanode/ui/components/accounts/account-logout-screen';
import { AccountLogoutContainer } from '@colanode/ui/components/accounts/account-logout-container';
import { AccountLogoutHeader } from '@colanode/ui/components/accounts/account-logout-header';
import { AccountLogoutTab } from '@colanode/ui/components/accounts/account-logout-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -11,10 +12,11 @@ import {
export const accountLogoutRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/account/logout',
component: AccountLogoutScreen,
component: AccountLogoutContainer,
context: () => {
return {
tab: <AccountLogoutTab />,
header: <AccountLogoutHeader />,
};
},
});

View File

@@ -1,6 +1,7 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { AccountSettingsScreen } from '@colanode/ui/components/accounts/account-settings-screen';
import { AccountSettingsContainer } from '@colanode/ui/components/accounts/account-settings-container';
import { AccountSettingsHeader } from '@colanode/ui/components/accounts/account-settings-header';
import { AccountSettingsTab } from '@colanode/ui/components/accounts/account-settings-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -11,10 +12,11 @@ import {
export const accountSettingsRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/account/settings',
component: AccountSettingsScreen,
component: AccountSettingsContainer,
context: () => {
return {
tab: <AccountSettingsTab />,
header: <AccountSettingsHeader />,
};
},
});

View File

@@ -1,6 +1,7 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { AppAppearanceSettingsScreen } from '@colanode/ui/components/app/app-appearance-settings-screen';
import { AppAppearanceSettingsContainer } from '@colanode/ui/components/app/app-appearance-settings-container';
import { AppAppearanceSettingsHeader } from '@colanode/ui/components/app/app-appearance-settings-header';
import { AppAppearanceSettingsTab } from '@colanode/ui/components/app/app-appearance-settings-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -11,10 +12,11 @@ import {
export const appAppearanceRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/app/appearance',
component: AppAppearanceSettingsScreen,
component: AppAppearanceSettingsContainer,
context: () => {
return {
tab: <AppAppearanceSettingsTab />,
header: <AppAppearanceSettingsHeader />,
};
},
});

View File

@@ -1,6 +1,7 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceDownloadsScreen } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-screen';
import { WorkspaceDownloadsContainer } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-container';
import { WorkspaceDownloadsHeader } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-header';
import { WorkspaceDownloadsTab } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -11,10 +12,11 @@ import {
export const workspaceDownloadsRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/downloads',
component: WorkspaceDownloadsScreen,
component: WorkspaceDownloadsContainer,
context: () => {
return {
tab: <WorkspaceDownloadsTab />,
header: <WorkspaceDownloadsHeader />,
};
},
});

View File

@@ -1,6 +1,7 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceHomeScreen } from '@colanode/ui/components/workspaces/workspace-home-screen';
import { WorkspaceHomeContainer } from '@colanode/ui/components/workspaces/workspace-home-container';
import { WorkspaceHomeHeader } from '@colanode/ui/components/workspaces/workspace-home-header';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
workspaceMaskRoute,
@@ -10,7 +11,12 @@ import {
export const workspaceHomeRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/home',
component: WorkspaceHomeScreen,
component: WorkspaceHomeContainer,
context: () => {
return {
header: <WorkspaceHomeHeader />,
};
},
});
export const workspaceHomeMaskRoute = createRoute({

Some files were not shown because too many files have changed in this diff Show More