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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
7 changes: 4 additions & 3 deletions packages/swingset/CLAUDE.md
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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