From a069ccb54c10b8beccc04f0bbb34fa841c398583 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Fri, 2 Oct 2026 16:30:34 -0600 Subject: [PATCH 1/4] refactor(swingset): align live navigation with component sidebar --- .../swingset/src/app/(clerk)/live-sidebar.tsx | 81 ++++++++++++------- .../src/app/(clerk)/live/live-overview.tsx | 6 +- .../swingset/src/components/app-sidebar.tsx | 21 +---- packages/swingset/src/lib/live-navigation.ts | 49 +++++++++++ .../swingset/src/lib/sidebar-navigation.ts | 21 +++++ 5 files changed, 127 insertions(+), 51 deletions(-) create mode 100644 packages/swingset/src/lib/live-navigation.ts create mode 100644 packages/swingset/src/lib/sidebar-navigation.ts diff --git a/packages/swingset/src/app/(clerk)/live-sidebar.tsx b/packages/swingset/src/app/(clerk)/live-sidebar.tsx index f8230b84c5e..611d4b874e8 100644 --- a/packages/swingset/src/app/(clerk)/live-sidebar.tsx +++ b/packages/swingset/src/app/(clerk)/live-sidebar.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import type { ComponentProps } from 'react'; +import { StatusDot } from '@/components/StatusDot'; import { Sidebar, SidebarContent, @@ -17,18 +18,11 @@ import { SidebarMenuItem, SidebarRail, } from '@/components/ui/sidebar'; +import { getLiveSidebarGroups } from '@/lib/live-navigation'; -const flows = [ - { title: 'Account section', href: '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/live/account-section' }, - { title: 'API keys', href: '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/live/api-keys' }, - { title: 'Password', href: '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/live/password' }, - { title: 'Connected accounts', href: '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/live/connected-accounts' }, - { title: 'Enterprise accounts', href: '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/live/enterprise-accounts' }, - { title: 'Delete account', href: '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/live/delete-account' }, - { title: 'Reverification', href: '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/live/reverification' }, -]; +const groups = getLiveSidebarGroups(); -function flowIsActive(pathname: string, href: string) { +function pageIsActive(pathname: string, href: string) { return pathname === href || pathname.startsWith(`${href}/`); } @@ -64,28 +58,57 @@ export function LiveSidebar(props: ComponentProps) { +

+ These are v1 wire-ups. They may still have bugs, even when they work. +

- - - Flows - - - - {flows.map(flow => ( - - } - > - {flow.title} - - + {groups.map(({ group, categories }) => ( + + + {group} + + + {categories.map(({ category, components }) => ( +
+ {category ? ( +
+ + {category} +
+ ) : null} + + {components.map(({ mod, href }) => ( + + } + > + {mod.meta.status ? ( + + ) : null} + {`<${mod.meta.title} />`} + + + ))} + +
))} -
-
-
+ + + ))} diff --git a/packages/swingset/src/app/(clerk)/live/live-overview.tsx b/packages/swingset/src/app/(clerk)/live/live-overview.tsx index 8199d9fca48..4cc7ce5e106 100644 --- a/packages/swingset/src/app/(clerk)/live/live-overview.tsx +++ b/packages/swingset/src/app/(clerk)/live/live-overview.tsx @@ -30,7 +30,7 @@ export function LiveOverview({

Live Sandbox

- Flows in this sandbox run against the real Clerk application configured in{' '} + These pages run v1 wire-ups against the real Clerk application configured in{' '} packages/swingset/.env.local.

@@ -91,7 +91,7 @@ export function LiveOverview({

Loading…

) : isSignedIn ? (

- Signed in as {user.primaryEmailAddress?.emailAddress ?? user.id}. Pick a flow from the sidebar. + Signed in as {user.primaryEmailAddress?.emailAddress ?? user.id}. Pick a page from the sidebar.

) : (

@@ -102,7 +102,7 @@ export function LiveOverview({ > Sign in {' '} - to run the flows. + to use the live pages.

)} diff --git a/packages/swingset/src/components/app-sidebar.tsx b/packages/swingset/src/components/app-sidebar.tsx index bc206465ab2..291d8544b4f 100644 --- a/packages/swingset/src/components/app-sidebar.tsx +++ b/packages/swingset/src/components/app-sidebar.tsx @@ -23,30 +23,13 @@ import { } from '@/components/ui/sidebar'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { getSidebarGroups } from '@/lib/registry'; +import { getSidebarCategories, type SidebarEntry } from '@/lib/sidebar-navigation'; import type { StoryStatus, WipSubstatus } from '@/lib/types'; const groups = getSidebarGroups(); const COLLAPSED_BY_DEFAULT = new Set(['Blocks', 'Primitives', 'Components', 'Styles', 'Hooks', 'Localization']); -type SidebarEntry = ReturnType[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, @@ -208,7 +191,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - {byCategory(components).map(({ category, components }) => + {getSidebarCategories(components).map(({ category, components }) => category ? ( ; +}; + +const liveRoutes = new Map([ + ['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/user-profile/user-profile-api-keys-panel', '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/live/api-keys'], + ['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/user-profile/user-profile-account-section', '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/live/account-section'], + ['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/user-profile/user-profile-password-section', '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/live/password'], + ['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/user-profile/user-profile-connected-accounts-section', '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/live/connected-accounts'], + ['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/user-profile/user-profile-enterprise-accounts-section', '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/live/enterprise-accounts'], + ['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/user-profile/user-profile-delete-section', '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/live/delete-account'], + ['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/reverification/reverification', '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/live/reverification'], +]); + +export function getLiveSidebarGroups(): LiveSidebarGroup[] { + const liveGroups: LiveSidebarGroup[] = []; + + for (const { group, groupSlug, components } of getSidebarGroups()) { + const categories: LiveSidebarGroup['categories'] = []; + + for (const { category, components: categoryComponents } of getSidebarCategories(components)) { + const liveComponents: LiveSidebarEntry[] = []; + + for (const component of categoryComponents) { + const href = liveRoutes.get(`/${groupSlug}/${component.componentSlug}`); + if (href) { + liveComponents.push({ ...component, href }); + } + } + + if (liveComponents.length > 0) { + categories.push({ category, components: liveComponents }); + } + } + + if (categories.length > 0) { + liveGroups.push({ group, groupSlug, categories }); + } + } + + return liveGroups; +} diff --git a/packages/swingset/src/lib/sidebar-navigation.ts b/packages/swingset/src/lib/sidebar-navigation.ts new file mode 100644 index 00000000000..499fb89c09c --- /dev/null +++ b/packages/swingset/src/lib/sidebar-navigation.ts @@ -0,0 +1,21 @@ +import type { getSidebarGroups } from '@/lib/registry'; + +export type SidebarEntry = ReturnType[number]['components'][number]; + +export type SidebarCategory = { category: string; components: SidebarEntry[] }; + +// 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. +export function getSidebarCategories(components: SidebarEntry[]): SidebarCategory[] { + const categories: SidebarCategory[] = []; + 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; +} From affd6f4058b1d5db967346b2f5f7b8dc381da47b Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Fri, 2 Oct 2026 17:00:11 -0600 Subject: [PATCH 2/4] refactor(swingset): share sidebar navigation rendering --- .../swingset/src/app/(clerk)/live-sidebar.tsx | 66 ++---- .../src/app/(clerk)/live/live-overview.tsx | 6 +- .../swingset/src/components/app-sidebar.tsx | 190 ++---------------- .../src/components/sidebar-navigation.tsx | 163 +++++++++++++++ 4 files changed, 203 insertions(+), 222 deletions(-) create mode 100644 packages/swingset/src/components/sidebar-navigation.tsx diff --git a/packages/swingset/src/app/(clerk)/live-sidebar.tsx b/packages/swingset/src/app/(clerk)/live-sidebar.tsx index 611d4b874e8..84a770c156b 100644 --- a/packages/swingset/src/app/(clerk)/live-sidebar.tsx +++ b/packages/swingset/src/app/(clerk)/live-sidebar.tsx @@ -5,13 +5,12 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import type { ComponentProps } from 'react'; -import { StatusDot } from '@/components/StatusDot'; +import { SidebarNavigation, type NavigationGroup } from '@/components/sidebar-navigation'; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, - SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, @@ -28,6 +27,19 @@ function pageIsActive(pathname: string, href: string) { export function LiveSidebar(props: ComponentProps) { 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 ( @@ -58,57 +70,9 @@ export function LiveSidebar(props: ComponentProps) { -

- These are v1 wire-ups. They may still have bugs, even when they work. -

- {groups.map(({ group, categories }) => ( - - - {group} - - - {categories.map(({ category, components }) => ( -
- {category ? ( -
- - {category} -
- ) : null} - - {components.map(({ mod, href }) => ( - - } - > - {mod.meta.status ? ( - - ) : null} - {`<${mod.meta.title} />`} - - - ))} - -
- ))} -
-
- ))} + diff --git a/packages/swingset/src/app/(clerk)/live/live-overview.tsx b/packages/swingset/src/app/(clerk)/live/live-overview.tsx index 4cc7ce5e106..8199d9fca48 100644 --- a/packages/swingset/src/app/(clerk)/live/live-overview.tsx +++ b/packages/swingset/src/app/(clerk)/live/live-overview.tsx @@ -30,7 +30,7 @@ export function LiveOverview({

Live Sandbox

- These pages run v1 wire-ups against the real Clerk application configured in{' '} + Flows in this sandbox run against the real Clerk application configured in{' '} packages/swingset/.env.local.

@@ -91,7 +91,7 @@ export function LiveOverview({

Loading…

) : isSignedIn ? (

- Signed in as {user.primaryEmailAddress?.emailAddress ?? user.id}. Pick a page from the sidebar. + Signed in as {user.primaryEmailAddress?.emailAddress ?? user.id}. Pick a flow from the sidebar.

) : (

@@ -102,7 +102,7 @@ export function LiveOverview({ > Sign in {' '} - to use the live pages. + to run the flows.

)} diff --git a/packages/swingset/src/components/app-sidebar.tsx b/packages/swingset/src/components/app-sidebar.tsx index 291d8544b4f..e64d529a773 100644 --- a/packages/swingset/src/components/app-sidebar.tsx +++ b/packages/swingset/src/components/app-sidebar.tsx @@ -1,19 +1,17 @@ '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 { SidebarNavigation, type NavigationGroup } from '@/components/sidebar-navigation'; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, - SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, @@ -21,92 +19,19 @@ import { SidebarRail, SidebarSeparator, } from '@/components/ui/sidebar'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { getSidebarGroups } from '@/lib/registry'; -import { getSidebarCategories, type SidebarEntry } from '@/lib/sidebar-navigation'; -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']); - -function SidebarUsageItem({ - usage, - href, - isActive, - status, - substatus, -}: { - usage: string; - href: string; - isActive: boolean; - status?: StoryStatus; - substatus?: WipSubstatus; -}) { - const labelRef = React.useRef(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 ( - - - } - > - {status ? ( - - ) : null} - - {usage} - - - } - /> - - {usage} - - - - ); -} - -function SidebarEntryMenu({ - components, - groupSlug, - pathname, -}: { - components: SidebarEntry[]; - groupSlug: string; - pathname: string; -}) { - return ( - - {components.map(({ mod, componentSlug }) => { +export function AppSidebar({ ...props }: React.ComponentProps) { + 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 @@ -118,23 +43,16 @@ function SidebarEntryMenu({ : mod.meta.group === 'Styles' || mod.meta.group === 'Localization' ? mod.meta.title : `<${mod.meta.title} />`; - return ( - - ); - })} - - ); -} - -export function AppSidebar({ ...props }: React.ComponentProps) { - const pathname = usePathname(); + return { + href, + usage, + isActive: pathname === href, + status: mod.meta.status, + substatus: mod.meta.substatus, + }; + }), + })), + })); return ( @@ -171,71 +89,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { Mosaic - Swingset - {groups.map(({ group, groupSlug, components }) => ( - - {group === 'Blocks' && } - - - } - > - {group} - - - - - {getSidebarCategories(components).map(({ category, components }) => - category ? ( - pathname === `/${groupSlug}/${componentSlug}`, - )} - className='group/category' - > - - - {category} - - - -
- -
-
-
- ) : ( - - ), - )} -
-
-
-
-
- ))} +
diff --git a/packages/swingset/src/components/sidebar-navigation.tsx b/packages/swingset/src/components/sidebar-navigation.tsx new file mode 100644 index 00000000000..5b7f100102a --- /dev/null +++ b/packages/swingset/src/components/sidebar-navigation.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { ChevronRightIcon } from 'lucide-react'; +import Link from 'next/link'; +import * as React from 'react'; + +import { StatusDot } from '@/components/StatusDot'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarSeparator, +} from '@/components/ui/sidebar'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import type { StoryStatus, WipSubstatus } from '@/lib/types'; + +type NavigationItem = { + href: string; + usage: string; + isActive: boolean; + status?: StoryStatus; + substatus?: WipSubstatus; +}; + +type NavigationCategory = { label: string; items: NavigationItem[] }; + +export type NavigationGroup = { + label: string; + separatorBefore?: boolean; + categories: NavigationCategory[]; +}; + +const COLLAPSED_BY_DEFAULT = new Set(['Blocks', 'Primitives', 'Components', 'Styles', 'Hooks', 'Localization']); + +function SidebarUsageItem({ usage, href, isActive, status, substatus }: NavigationItem) { + const labelRef = React.useRef(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 ( + + + } + > + {status ? ( + + ) : null} + + {usage} + + + } + /> + + {usage} + + + + ); +} + +function NavigationItems({ items }: { items: NavigationItem[] }) { + return ( + + {items.map(item => ( + + ))} + + ); +} + +export function SidebarNavigation({ groups }: { groups: NavigationGroup[] }) { + return groups.map(({ label, separatorBefore, categories }) => ( + + {separatorBefore && } + + + } + > + {label} + + + + + {categories.map(({ label: category, items }) => + category ? ( + item.isActive)} + className='group/category' + > + + + {category} + + + +
+ +
+
+
+ ) : ( + + ), + )} +
+
+
+
+
+ )); +} From eec50195ded45cb496aab694a30c301cc25e2e2b Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Fri, 2 Oct 2026 17:06:18 -0600 Subject: [PATCH 3/4] fix(swingset): sort sidebar navigation imports --- packages/swingset/src/app/(clerk)/live-sidebar.tsx | 2 +- packages/swingset/src/components/app-sidebar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/swingset/src/app/(clerk)/live-sidebar.tsx b/packages/swingset/src/app/(clerk)/live-sidebar.tsx index 84a770c156b..f159adc029d 100644 --- a/packages/swingset/src/app/(clerk)/live-sidebar.tsx +++ b/packages/swingset/src/app/(clerk)/live-sidebar.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import type { ComponentProps } from 'react'; -import { SidebarNavigation, type NavigationGroup } from '@/components/sidebar-navigation'; +import { type NavigationGroup, SidebarNavigation } from '@/components/sidebar-navigation'; import { Sidebar, SidebarContent, diff --git a/packages/swingset/src/components/app-sidebar.tsx b/packages/swingset/src/components/app-sidebar.tsx index e64d529a773..a2c55d3d321 100644 --- a/packages/swingset/src/components/app-sidebar.tsx +++ b/packages/swingset/src/components/app-sidebar.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import * as React from 'react'; -import { SidebarNavigation, type NavigationGroup } from '@/components/sidebar-navigation'; +import { type NavigationGroup, SidebarNavigation } from '@/components/sidebar-navigation'; import { Sidebar, SidebarContent, From 70a47234c7a6d6202abd977f4c0e296be4002568 Mon Sep 17 00:00:00 2001 From: austincalvelage Date: Sat, 3 Oct 2026 11:56:42 -0600 Subject: [PATCH 4/4] chore: add empty changeset --- .changeset/crisp-rabbits-wave.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/crisp-rabbits-wave.md diff --git a/.changeset/crisp-rabbits-wave.md b/.changeset/crisp-rabbits-wave.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/crisp-rabbits-wave.md @@ -0,0 +1,2 @@ +--- +---