Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
fix(ui): User Panel rough in#9482
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
6d675c9123bf5fe18d1df291cf0d48aa225f09b9bd22539b75a803739d0c83b76bf81121a185d951af3c485bad23ec6b3babe74f3920e3e84e0835f8750954c781a71bd063a25caec63e9a1c6daf9ac32e3d10fa3fd96c2ea35017c42a87a20dc863eb7a34d9df9ba3819681b83ef85728f2e2103867ff16845f535cf642a13a334a505f3c874df6c8adb2237eb52d4665aca3e9e1fc4d21d04cFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -7,13 +7,13 @@ export interface CompositionPiece { | ||
| name: string; | ||
| /** Route to the piece's page in swingset (e.g. `/components/button`). */ | ||
| href: string; | ||
| /** Which Mosaic layer the piece lives in (e.g. `User`, `Components`, `Primitives`). */ | ||
| /** Which Mosaic layer the piece lives in (e.g. `User Button`, `Components`, `Primitives`). */ | ||
| layer: string; | ||
| } | ||
| // Mosaic layers, high → low. Drives the order the composition groups render in. | ||
| // Matches the sidebar group names. | ||
| const LAYER_ORDER = ['User', 'Components', 'Styles', 'Primitives']; | ||
| const LAYER_ORDER = ['User Button', 'User Profile', 'Components', 'Styles', 'Primitives']; | ||
austincalvelage marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| function layerRank(layer: string): number { | ||
| const i = LAYER_ORDER.indexOf(layer); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| 'use client'; | ||
| import { ChevronRightIcon } from 'lucide-react'; | ||
| import Link from 'next/link'; | ||
| import { usePathname } from 'next/navigation'; | ||
| import * as React from 'react'; | ||
| import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; | ||
| import { | ||
| Sidebar, | ||
| SidebarContent, | ||
| @@ -15,11 +17,116 @@ import { | ||
| SidebarMenuButton, | ||
| SidebarMenuItem, | ||
| SidebarRail, | ||
| SidebarSeparator, | ||
| } from '@/components/ui/sidebar'; | ||
| import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; | ||
| import { getSidebarGroups } from '@/lib/registry'; | ||
| const groups = getSidebarGroups(); | ||
| const COLLAPSED_BY_DEFAULT = new Set(['Primitives', 'Components', 'Styles', 'Hooks']); | ||
| 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 }: { usage: string; href: string; isActive: boolean }) { | ||
| 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} />} | ||
| > | ||
| <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 }) => { | ||
| 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 | ||
| // exports with no single call form worth privileging, and everything else is a | ||
| // component rendered as JSX. | ||
| const usage = | ||
| mod.meta.group === 'Hooks' | ||
| ? `${mod.meta.title}()` | ||
| : mod.meta.group === 'Styles' | ||
| ? mod.meta.title | ||
| : `<${mod.meta.title} />`; | ||
| return ( | ||
| <SidebarUsageItem | ||
| key={mod.meta.title} | ||
| usage={usage} | ||
| href={href} | ||
| isActive={pathname === href} | ||
| /> | ||
| ); | ||
| })} | ||
| </SidebarMenu> | ||
| ); | ||
| } | ||
| export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { | ||
| const pathname = usePathname(); | ||
| @@ -59,45 +166,69 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { | ||
| </SidebarHeader> | ||
| <SidebarContent className='gap-0'> | ||
| {groups.map(({ group, groupSlug, components }) => ( | ||
| <SidebarGroup | ||
| key={group} | ||
| className='py-1' | ||
| data-section={group} | ||
| > | ||
| <SidebarGroupLabel className='text-sidebar-foreground/50 h-auto px-2 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wider'> | ||
| {group} | ||
| </SidebarGroupLabel> | ||
| <SidebarGroupContent> | ||
| <SidebarMenu> | ||
| {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 | ||
| // exports with no single call form worth privileging, and everything else is a | ||
| // component rendered as JSX. | ||
| const usage = | ||
| mod.meta.group === 'Hooks' | ||
| ? `${mod.meta.title}()` | ||
| : mod.meta.group === 'Styles' | ||
| ? mod.meta.title | ||
| : `<${mod.meta.title} />`; | ||
| return ( | ||
| <SidebarMenuItem key={mod.meta.title}> | ||
| <SidebarMenuButton | ||
| className='h-auto items-start py-1 text-xs leading-relaxed' | ||
| isActive={pathname === href} | ||
| render={<Link href={href} />} | ||
| > | ||
| <span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'> | ||
| {usage} | ||
| </span> | ||
| </SidebarMenuButton> | ||
| </SidebarMenuItem> | ||
| ); | ||
| })} | ||
| </SidebarMenu> | ||
| </SidebarGroupContent> | ||
| </SidebarGroup> | ||
| <React.Fragment key={group}> | ||
| {group === 'Components' && <SidebarSeparator className='data-horizontal:w-auto my-1' />} | ||
| <Collapsible | ||
| defaultOpen={!COLLAPSED_BY_DEFAULT.has(group)} | ||
| className='group/collapsible' | ||
| > | ||
austincalvelage marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| <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> | ||
| ))} | ||
| </SidebarContent> | ||
| <SidebarRail /> | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.