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
2 changes: 2 additions & 0 deletions .changeset/crisp-rabbits-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
49 changes: 18 additions & 31 deletions packages/swingset/src/app/(clerk)/live-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,41 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { ComponentProps } from 'react';

import { type NavigationGroup, SidebarNavigation } from '@/components/sidebar-navigation';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
} from '@/components/ui/sidebar';
import { getLiveSidebarGroups } from '@/lib/live-navigation';

const flows = [
{ title: 'Account section', href: '/live/account-section' },
{ title: 'API keys', href: '/live/api-keys' },
{ title: 'Password', href: '/live/password' },
{ title: 'Connected accounts', href: '/live/connected-accounts' },
{ title: 'Enterprise accounts', href: '/live/enterprise-accounts' },
{ title: 'Delete account', href: '/live/delete-account' },
{ title: 'Reverification', href: '/live/reverification' },
];
const groups = getLiveSidebarGroups();

function flowIsActive(pathname: string, href: string) {
function pageIsActive(pathname: string, href: string) {
return pathname === href || pathname.startsWith(`${href}/`);
}

export function LiveSidebar(props: ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
const navigationGroups: NavigationGroup[] = groups.map(({ group, categories }) => ({
label: group,
categories: categories.map(({ category, components }) => ({
label: category,
items: components.map(({ mod, href }) => ({
href,
usage: `<${mod.meta.title} />`,
isActive: pageIsActive(pathname, href),
status: mod.meta.status,
substatus: mod.meta.substatus,
})),
})),
}));

return (
<Sidebar {...props}>
Expand Down Expand Up @@ -66,26 +72,7 @@ export function LiveSidebar(props: ComponentProps<typeof Sidebar>) {
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup className='py-1'>
<SidebarGroupLabel className='text-sidebar-foreground/50 h-auto px-2 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wider'>
Flows
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{flows.map(flow => (
<SidebarMenuItem key={flow.href}>
<SidebarMenuButton
className='h-auto py-1 text-xs'
isActive={flowIsActive(pathname, flow.href)}
render={<Link href={flow.href} />}
>
{flow.title}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarNavigation groups={navigationGroups} />
</SidebarContent>
<SidebarRail />
</Sidebar>
Expand Down
207 changes: 22 additions & 185 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,37 @@
'use client';

import { ChevronRightIcon, FlaskConicalIcon } from 'lucide-react';
import { FlaskConicalIcon } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import * as React from 'react';

import { StatusDot } from '@/components/StatusDot';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { type NavigationGroup, SidebarNavigation } from '@/components/sidebar-navigation';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
} from '@/components/ui/sidebar';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { getSidebarGroups } from '@/lib/registry';
import type { StoryStatus, WipSubstatus } from '@/lib/types';
import { getSidebarCategories } from '@/lib/sidebar-navigation';

const groups = getSidebarGroups();

const COLLAPSED_BY_DEFAULT = new Set(['Blocks', 'Primitives', 'Components', 'Styles', 'Hooks', 'Localization']);

type SidebarEntry = ReturnType<typeof getSidebarGroups>[number]['components'][number];

// Partitions a group's entries by `meta.navigation.category` into subheaded runs. Category and
// entry order both follow first appearance in the registry; uncategorized entries get no subheading.
function byCategory(components: SidebarEntry[]) {
const categories: { category: string; components: SidebarEntry[] }[] = [];
for (const component of components) {
const category = component.mod.meta.navigation?.category ?? '';
const bucket = categories.find(c => c.category === category);
if (bucket) {
bucket.components.push(component);
} else {
categories.push({ category, components: [component] });
}
}
return categories;
}

function SidebarUsageItem({
usage,
href,
isActive,
status,
substatus,
}: {
usage: string;
href: string;
isActive: boolean;
status?: StoryStatus;
substatus?: WipSubstatus;
}) {
const labelRef = React.useRef<HTMLSpanElement>(null);
const [isTruncated, setIsTruncated] = React.useState(false);

React.useEffect(() => {
const label = labelRef.current;
if (!label) {
return;
}
const check = () => setIsTruncated(label.scrollWidth > label.clientWidth);
check();
const observer = new ResizeObserver(check);
observer.observe(label);
return () => observer.disconnect();
}, []);

return (
<SidebarMenuItem>
<Tooltip disabled={!isTruncated}>
<TooltipTrigger
delay={300}
render={
<SidebarMenuButton
className='h-auto py-1 text-xs'
isActive={isActive}
render={<Link href={href} />}
>
{status ? (
<StatusDot
status={status}
substatus={substatus}
/>
) : null}
<span
ref={labelRef}
className='truncate font-mono text-[10px] leading-relaxed'
>
{usage}
</span>
</SidebarMenuButton>
}
/>
<TooltipContent
side='right'
className='font-mono text-[10px]'
>
{usage}
</TooltipContent>
</Tooltip>
</SidebarMenuItem>
);
}

function SidebarEntryMenu({
components,
groupSlug,
pathname,
}: {
components: SidebarEntry[];
groupSlug: string;
pathname: string;
}) {
return (
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
const navigationGroups: NavigationGroup[] = groups.map(({ group, groupSlug, components }) => ({
label: group,
separatorBefore: group === 'Blocks',
categories: getSidebarCategories(components).map(({ category, components }) => ({
label: category,
items: components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// How an entry is USED differs by layer, so the label follows the layer rather
// than a guess at the title: hooks are called, atomic styles are a set of
Expand All @@ -135,23 +43,16 @@ function SidebarEntryMenu({
: mod.meta.group === 'Styles' || mod.meta.group === 'Localization'
? mod.meta.title
: `<${mod.meta.title} />`;
return (
<SidebarUsageItem
key={mod.meta.title}
usage={usage}
href={href}
isActive={pathname === href}
status={mod.meta.status}
substatus={mod.meta.substatus}
/>
);
})}
</SidebarMenu>
);
}

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();
return {
href,
usage,
isActive: pathname === href,
status: mod.meta.status,
substatus: mod.meta.substatus,
};
}),
})),
}));

return (
<Sidebar {...props}>
Expand Down Expand Up @@ -188,71 +89,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<span className='text-sidebar-foreground/70 text-[10px] font-medium'>Mosaic - Swingset</span>
</SidebarHeader>
<SidebarContent className='gap-0'>
{groups.map(({ group, groupSlug, components }) => (
<React.Fragment key={group}>
{group === 'Blocks' && <SidebarSeparator className='data-horizontal:w-auto my-1' />}
<Collapsible
defaultOpen={!COLLAPSED_BY_DEFAULT.has(group)}
className='group/collapsible'
>
<SidebarGroup
className='py-1'
data-section={group}
>
<SidebarGroupLabel
className='text-sidebar-foreground/50 hover:text-sidebar-foreground/80 h-auto w-full px-2 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wider'
render={<CollapsibleTrigger />}
>
{group}
<ChevronRightIcon className='size-3! ml-auto transition-transform group-data-[open]/collapsible:rotate-90' />
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
{byCategory(components).map(({ category, components }) =>
category ? (
<Collapsible
key={category}
// Collapsed by default, unless it holds the page being viewed.
defaultOpen={components.some(
({ componentSlug }) => pathname === `/${groupSlug}/${componentSlug}`,
)}
className='group/category'
>
<CollapsibleTrigger className='text-sidebar-foreground/40 hover:text-sidebar-foreground/70 flex w-full items-center gap-1 px-2 pb-0.5 pt-2 text-[9px] font-semibold uppercase tracking-wider'>
<span
aria-hidden='true'
className='font-mono text-[10px] leading-none'
>
└
</span>
{category}
<ChevronRightIcon className='size-2.5! ml-auto transition-transform group-data-[open]/category:rotate-90' />
</CollapsibleTrigger>
<CollapsibleContent>
<div className='border-sidebar-border ml-3 border-l pl-1'>
<SidebarEntryMenu
components={components}
groupSlug={groupSlug}
pathname={pathname}
/>
</div>
</CollapsibleContent>
</Collapsible>
) : (
<SidebarEntryMenu
key={group}
components={components}
groupSlug={groupSlug}
pathname={pathname}
/>
),
)}
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
</React.Fragment>
))}
<SidebarNavigation groups={navigationGroups} />
</SidebarContent>
<SidebarFooter className='gap-0 p-0'>
<SidebarSeparator className='data-horizontal:w-auto my-1' />
Expand Down
Loading
Loading