Skip to content
Closed
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/swingset-sidebar-collapse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ Pick the archetype below by the component's **layer** (its `meta.group`), then f

| Group | What lives here | Archetype |
| ------------ | -------------------------------------------------------------- | --------- |
| `User` | Composed flow UI (e.g. `UserButton`) | C |
| `User Button` | Composed flow UI (e.g. `UserButton`) | C |
| `User Profile` | Composed flow UI (e.g. `UserProfileProfilePanel`) | C |
| `Components` | Styled Mosaic components — simple, with a flat variant surface (`Button`, `Input`), or compound (`Card`, `Field`, `Menu`, `Popover`) | A |
| `Primitives` | Headless `@clerk/headless` primitives (`Accordion`) | B |
| `Styles` | Atomic styles that ship as StyleX atoms, not components (`Scroll Area`) | B (adapted) |
| `Hooks` | Headless hooks (`useDataTable`) | B (adapted) |

`User` → `Components` → `Primitives` runs high-level-composition → low-level-primitive. Composed layers are documented as compositions of lower layers (archetype C); leaf layers (Components, Primitives) get full prop/knob docs (archetypes A and B).
`User Button` / `User Profile` → `Components` → `Primitives` runs high-level-composition → low-level-primitive. Composed layers are documented as compositions of lower layers (archetype C); leaf layers (Components, Primitives) get full prop/knob docs (archetypes A and B).

`Styles` and `Hooks` are the non-component layers: there is no element to knob, so they follow
archetype B's shape (Example → Usage → Parts → Styling) with `Props` replaced by whatever the export
Expand Down Expand Up @@ -239,7 +240,7 @@ The story is `meta` (no `styles`) plus a single `Default` export that renders th

**Document the default value for every prop in a dedicated Default column.** Every props table — auto and hand-written — has a **Default** column; the `Type` stays a plain union/enum and the default is named in its own column (the convention every component-doc site and TypeDoc's `@default` tag follow), never inlined into the type. The auto `<PropTable>` renders `Prop | Type | Default | Value` and fills Default from `meta.styles._defaultVariants` (the **Value** column is the live knob seeded with that default); hand-written tables render `Prop | Type | Default | Description` and fill it by hand. Name the default member (`'base'`, `'multiple'`, `'bottom-start'`); use `—` when there is no default (a controlled-only or required prop) and append `(required)` for required props; when the default is behavioral rather than a literal, state it in words (`inherits Root`, `falls back to value`).

### Archetype C — composed layer (`User`)
### Archetype C — composed layer (`User Button`, `User Profile`)

These compose lower layers, so the docs lead with the composition rather than knobs. Required MDX:

Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
4 changes: 3 additions & 1 deletion packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { ViewSource } from './ViewSource';
// MDX docs keyed by `group` slug → `component` slug. Group-aware so identically-named
// entries (the headless `Dialog` primitive vs. the styled `Dialog` component) stay distinct.
const docModules: Record<string, Record<string, React.ComponentType>> = {
user: {
'user-button': {
'user-button': dynamic(() => import('../stories/user-button.mdx')),
},
'user-profile': {
'user-profile-profile-panel': dynamic(() => import('../stories/user-profile-profile-panel.mdx')),
'user-profile-account-section': dynamic(() => import('../stories/user-profile-account-section.mdx')),
'user-profile-connected-accounts-section': dynamic(
Expand Down
137 changes: 98 additions & 39 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'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 { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
Sidebar,
SidebarContent,
Expand All @@ -15,11 +18,59 @@ import {
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
} from '@/components/ui/sidebar';
import { getSidebarGroups } from '@/lib/registry';

const groups = getSidebarGroups();

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

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>
);
}

export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname();

Expand Down Expand Up @@ -59,45 +110,53 @@ 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'
>
<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>
<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>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
</React.Fragment>
))}
</SidebarContent>
<SidebarRail />
Expand Down
3 changes: 2 additions & 1 deletion packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,9 @@ const userProfileDeleteSectionModule: StoryModule = {
};

export const registry: StoryModule[] = [
// User
// User Button
userButtonModule,
// User Profile
userProfileProfilePanelModule,
userProfileAccountSectionModule,
userProfileConnectedAccountsSectionModule,
Expand Down
2 changes: 1 addition & 1 deletion packages/swingset/src/stories/user-button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { StoryMeta } from '@/lib/types';
export { default as __source } from './user-button.stories?raw';

export const meta: StoryMeta = {
group: 'User',
group: 'User Button',
title: 'UserButton',
source: 'packages/ui/src/mosaic/user-button/user-button.view.tsx',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StoryMeta } from '@/lib/types';
export { default as __source } from './user-profile-account-section.stories?raw';

export const meta: StoryMeta = {
group: 'User',
group: 'User Profile',
title: 'UserProfileAccountSection',
source: 'packages/ui/src/mosaic/user-profile/user-profile-account-section.view.tsx',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StoryMeta } from '@/lib/types';
export { default as __source } from './user-profile-connected-accounts-section.stories?raw';

export const meta: StoryMeta = {
group: 'User',
group: 'User Profile',
title: 'UserProfileConnectedAccountsSection',
source: 'packages/ui/src/mosaic/user-profile/user-profile-connected-accounts-section.view.tsx',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StoryMeta } from '@/lib/types';
export { default as __source } from './user-profile-delete-section.stories?raw';

export const meta: StoryMeta = {
group: 'User',
group: 'User Profile',
title: 'UserProfileDeleteSection',
source: 'packages/ui/src/mosaic/user-profile/user-profile-delete-section.view.tsx',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const profileImageUrl = 'https://avatars.githubusercontent.com/u/51144033?v=4';
export { default as __source } from './user-profile-profile-panel.stories?raw';

export const meta: StoryMeta = {
group: 'User',
group: 'User Profile',
title: 'UserProfileProfilePanel',
source: 'packages/ui/src/mosaic/user-profile/user-profile-profile-panel.view.tsx',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { StoryMeta } from '@/lib/types';
export { default as __source } from './user-profile-web3-wallets-section.stories?raw';

export const meta: StoryMeta = {
group: 'User',
group: 'User Profile',
title: 'UserProfileWeb3WalletsSection',
source: 'packages/ui/src/mosaic/user-profile/user-profile-web3-wallets-section.view.tsx',
};
Expand Down
Loading