Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/docs/content/connections/vault.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ written.

### Add an entry

Open **Vault** in the sidebar and choose **Add entry**. Once anyone else is in the organization, or
invited to it, **Just me** and **Everyone in** the organization pick which vault is open, and a new
entry goes in that one.
Open **Connections** in the sidebar, then **Vault**, first in the list, and choose **Add entry**.
Once anyone else is in the organization, or invited to it, **Just me** and **Everyone in** the
organization pick which vault is open, and a new entry goes in that one.

### Choose what agents can do

Expand Down
11 changes: 6 additions & 5 deletions packages/docs/content/organizations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ with everyone in it or one member's own, its activity log, its vault and each me
starts with one of their own, and can make more, such as one for work beside a personal one, or
join someone else's. Every page is one organization's, named first in its address
(`/acme/connections`), so a link to a page is a link to it in that organization; the mark at the
top of the sidebar is the one you are in: hover it to switch, or to create another. Your agents
there reach what is connected there.
top of the sidebar is the one you are in: hover it to switch, to open its **Members** or
**Settings**, or to create another. Your agents there reach what is connected there.

## Your own or everyone's

Expand Down Expand Up @@ -56,9 +56,10 @@ page on a shared connection is logged with their name.

## Members

**Members** lists who is in the organization and who is invited. Every organization has one owner,
whoever made it, for good; everyone else is a member. The owner invites people by email, removes
members, renames the organization and deletes it. Members use everything in it and can leave.
**Members**, under the organization's mark at the top of the sidebar, lists who is in the
organization and who is invited. Every organization has one owner, whoever made it, for good;
everyone else is a member. The owner invites people by email, removes members, renames the
organization and deletes it. Members use everything in it and can leave.

An invitation is a link that works for a week, for the address it was sent to: the person signs in,
or creates an account, as that address to join. Inviting follows the same rules as signing up, so
Expand Down
18 changes: 0 additions & 18 deletions packages/ui/src/components/separator.tsx

This file was deleted.

230 changes: 11 additions & 219 deletions packages/ui/src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,25 @@ import {cn} from 'cn';

import {useIsMobile} from '@proxy/ui/hooks/use-mobile';
import {Button} from '@proxy/ui/components/button';
import {Input} from '@proxy/ui/components/input';
import {Separator} from '@proxy/ui/components/separator';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@proxy/ui/components/sheet';
import {Skeleton} from '@proxy/ui/components/skeleton';
import {Tooltip, TooltipContent, TooltipTrigger} from '@proxy/ui/components/tooltip';
import {PanelLeftIcon} from 'lucide-react';

const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';

type SidebarContextProps = {
state: 'expanded' | 'collapsed';
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};

const SidebarContext = React.createContext<SidebarContextProps | null>(null);
Expand All @@ -49,74 +40,24 @@ function useSidebar() {
return context;
}

// On desktop the sidebar is either always collapsed or always expanded, as `collapsed` says: there
// is no toggling it. On phones it is a sheet, opened by the trigger and closed on every pick.
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
collapsed = false,
className,
style,
children,
...props
}: React.ComponentProps<'div'> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
collapsed?: boolean;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);

// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === 'function' ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}

// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);

// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]);

// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};

window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);

// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed';
const state = collapsed ? 'collapsed' : 'expanded';

const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
() => ({state, isMobile, openMobile, setOpenMobile}),
[state, isMobile, openMobile],
);

return (
Expand Down Expand Up @@ -244,8 +185,9 @@ function Sidebar({
);
}

// Opens the sidebar's sheet on phones.
function SidebarTrigger({className, onClick, ...props}: React.ComponentProps<typeof Button>) {
const {toggleSidebar} = useSidebar();
const {openMobile, setOpenMobile} = useSidebar();

return (
<Button
Expand All @@ -256,65 +198,16 @@ function SidebarTrigger({className, onClick, ...props}: React.ComponentProps<typ
className={cn(className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
setOpenMobile(!openMobile);
}}
{...props}
>
<PanelLeftIcon className="cn-rtl-flip" />
<span className="sr-only">Toggle Sidebar</span>
<span className="sr-only">Open sidebar</span>
</Button>
);
}

function SidebarRail({className, ...props}: React.ComponentProps<'button'>) {
const {toggleSidebar} = useSidebar();

return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className,
)}
{...props}
/>
);
}

function SidebarInset({className, ...props}: React.ComponentProps<'main'>) {
return (
<main
data-slot="sidebar-inset"
className={cn(
'relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
className,
)}
{...props}
/>
);
}

function SidebarInput({className, ...props}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn('h-8 w-full bg-background shadow-none', className)}
{...props}
/>
);
}

function SidebarHeader({className, ...props}: React.ComponentProps<'div'>) {
return (
<div
Expand All @@ -337,17 +230,6 @@ function SidebarFooter({className, ...props}: React.ComponentProps<'div'>) {
);
}

function SidebarSeparator({className, ...props}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn('mx-2 w-auto bg-sidebar-border', className)}
{...props}
/>
);
}

function SidebarContent({className, ...props}: React.ComponentProps<'div'>) {
return (
<div
Expand Down Expand Up @@ -397,41 +279,6 @@ function SidebarGroupLabel({
});
}

function SidebarGroupAction({
className,
render,
...props
}: useRender.ComponentProps<'button'> & React.ComponentProps<'button'>) {
return useRender({
defaultTagName: 'button',
props: mergeProps<'button'>(
{
className: cn(
'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
className,
),
},
props,
),
render,
state: {
slot: 'sidebar-group-action',
sidebar: 'group-action',
},
});
}

function SidebarGroupContent({className, ...props}: React.ComponentProps<'div'>) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn('w-full text-sm', className)}
{...props}
/>
);
}

function SidebarMenu({className, ...props}: React.ComponentProps<'ul'>) {
return (
<ul
Expand Down Expand Up @@ -560,53 +407,6 @@ function SidebarMenuAction({
});
}

function SidebarMenuBadge({className, ...props}: React.ComponentProps<'div'>) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
'pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground',
className,
)}
{...props}
/>
);
}

function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<'div'> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const [width] = React.useState(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
});

return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width,
} as React.CSSProperties
}
/>
</div>
);
}

function SidebarMenuSub({className, ...props}: React.ComponentProps<'ul'>) {
return (
<ul
Expand Down Expand Up @@ -669,24 +469,16 @@ export {
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};
Loading
Loading