Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(ui): Add mosaic organization profile components by alexcarpenter · Pull Request #9045 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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/open-heads-brush.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 2 additions & 2 deletions packages/swingset/src/components/Composition.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,8 +12,8 @@ export interface CompositionPiece {
}

// Mosaic layers, high → low. Drives the order the composition groups render in.
// Plural to match the sidebar group names.
const LAYER_ORDER = ['AIO', 'Panels', 'Sections', 'Blocks', 'Components', 'Primitives'];
// Matches the sidebar group names.
const LAYER_ORDER = ['Organization', 'Blocks', 'Components', 'Primitives'];

function layerRank(layer: string): number {
const i = LAYER_ORDER.indexOf(layer);
Expand Down
12 changes: 4 additions & 8 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,15 +10,11 @@ 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>> = {
aio: {
organization: {
'organization-profile': dynamic(() => import('../stories/organization-profile.mdx')),
},
panels: {
'organization-profile-general': dynamic(() => import('../stories/organization-profile-general.mdx')),
},
sections: {
'leave-organization': dynamic(() => import('../stories/leave-organization.mdx')),
'delete-organization': dynamic(() => import('../stories/delete-organization.mdx')),
'organization-profile-general-panel': dynamic(() => import('../stories/organization-profile-general-panel.mdx')),
'organization-profile-leave-section': dynamic(() => import('../stories/organization-profile-leave-section.mdx')),
'organization-profile-delete-section': dynamic(() => import('../stories/organization-profile-delete-section.mdx')),
},
blocks: {
destructive: dynamic(() => import('../stories/destructive.mdx')),
Expand Down
11 changes: 7 additions & 4 deletions packages/swingset/src/components/app-sidebar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -71,16 +71,19 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarMenu>
{components.map(({ mod, componentSlug }) => {
const href = `/${groupSlug}/${componentSlug}`;
// Hooks (e.g. `useDataTable`) are called, not rendered — show `useX()` rather
// than JSX `<useX />`. Everything else is a component.
const isHook = /^use[A-Z]/.test(mod.meta.title);
const usage = isHook ? `${mod.meta.title}()` : `<${mod.meta.title} />`;
return (
<SidebarMenuItem key={mod.meta.title}>
<SidebarMenuButton
className='h-auto justify-between py-1 text-xs leading-relaxed'
className='h-auto items-start py-1 text-xs leading-relaxed'
isActive={pathname === href}
render={<Link href={href} />}
>
<span className='truncate'>{mod.meta.label ?? mod.meta.title}</span>
<span className='text-sidebar-foreground/50 shrink-0 font-mono text-[10px] leading-none'>
{`<${mod.meta.title} />`}
<span className='whitespace-normal! break-all font-mono text-[10px] leading-relaxed'>
{usage}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down
48 changes: 26 additions & 22 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,10 +8,6 @@ import {
meta as cardComponentMeta,
} from '../stories/card.component.stories';
import { meta as collapsibleMeta } from '../stories/collapsible.stories';
import {
Default as DeleteOrganizationDefault,
meta as deleteOrganizationMeta,
} from '../stories/delete-organization.stories';
import { Default as DestructiveDefault, meta as destructiveMeta } from '../stories/destructive.stories';
import { Default as DialogDefault, meta as dialogComponentMeta } from '../stories/dialog.component.stories';
import { meta as dialogMeta } from '../stories/dialog.stories';
Expand All@@ -36,19 +32,23 @@ import {
meta as inputMeta,
Sizes as InputSizes,
} from '../stories/input.stories';
import {
Default as LeaveOrganizationDefault,
meta as leaveOrganizationMeta,
} from '../stories/leave-organization.stories';
import { meta as menuMeta } from '../stories/menu.stories';
import {
Default as OrganizationProfileDefault,
meta as organizationProfileMeta,
} from '../stories/organization-profile.stories';
import {
Default as OrganizationProfileGeneralDefault,
meta as organizationProfileGeneralMeta,
} from '../stories/organization-profile-general.stories';
Default as OrganizationProfileDeleteSectionDefault,
meta as organizationProfileDeleteSectionMeta,
} from '../stories/organization-profile-delete-section.stories';
import {
Default as OrganizationProfileGeneralPanelDefault,
meta as organizationProfileGeneralPanelMeta,
} from '../stories/organization-profile-general-panel.stories';
import {
Default as OrganizationProfileLeaveSectionDefault,
meta as organizationProfileLeaveSectionMeta,
} from '../stories/organization-profile-leave-section.stories';
import { meta as otpMeta } from '../stories/otp.stories';
import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
Expand All@@ -66,12 +66,18 @@ import { toSlug } from './slug';
import type { StoryModule } from './types';

const destructiveModule: StoryModule = { meta: destructiveMeta, Default: DestructiveDefault };
const leaveOrganizationModule: StoryModule = { meta: leaveOrganizationMeta, Default: LeaveOrganizationDefault };
const deleteOrganizationModule: StoryModule = { meta: deleteOrganizationMeta, Default: DeleteOrganizationDefault };
const organizationProfileLeaveSectionModule: StoryModule = {
meta: organizationProfileLeaveSectionMeta,
Default: OrganizationProfileLeaveSectionDefault,
};
const organizationProfileDeleteSectionModule: StoryModule = {
meta: organizationProfileDeleteSectionMeta,
Default: OrganizationProfileDeleteSectionDefault,
};
const organizationProfileModule: StoryModule = { meta: organizationProfileMeta, Default: OrganizationProfileDefault };
const organizationProfileGeneralModule: StoryModule = {
meta: organizationProfileGeneralMeta,
Default: OrganizationProfileGeneralDefault,
const organizationProfileGeneralPanelModule: StoryModule = {
meta: organizationProfileGeneralPanelMeta,
Default: OrganizationProfileGeneralPanelDefault,
};

const cardComponentModule: StoryModule = { meta: cardComponentMeta, Default: CardDefault, Centered: CardCentered };
Expand DownExpand Up@@ -119,13 +125,11 @@ const tooltipModule: StoryModule = { meta: tooltipMeta };
const useDataTableModule: StoryModule = { meta: useDataTableMeta };

export const registry: StoryModule[] = [
// AIO
// Organization
organizationProfileModule,
// Panels
organizationProfileGeneralModule,
// Sections
leaveOrganizationModule,
deleteOrganizationModule,
organizationProfileGeneralPanelModule,
organizationProfileLeaveSectionModule,
organizationProfileDeleteSectionModule,
// Blocks
destructiveModule,
// Components
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/delete-organization.stories.tsx

This file was deleted.

41 changes: 22 additions & 19 deletions packages/swingset/src/stories/icon.stories.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
/** @jsxImportSource @emotion/react */
import type { MosaicIconRenderer } from '@clerk/ui/mosaic/appearance';
import type { IconProps } from '@clerk/ui/mosaic/components/icon';
import { Icon, iconRecipe } from '@clerk/ui/mosaic/components/icon';
import { iconRegistry } from '@clerk/ui/mosaic/icons/registry';
Expand DownExpand Up@@ -75,26 +74,30 @@ export function Names() {
);
}

// Mosaic's styling (sizing/color) applies to the override just like the built-in glyph, so the
// replacement only needs its viewBox + paths — spread `props` to receive the sizing className and
// `data-cl-slot`. Defined at module scope (not inline in the story) to keep a stable component type.
const CircleGlyph: MosaicIconRenderer = props => (
<svg
{...props}
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
);

export function Override() {
return (
<MosaicProvider appearance={{ icons: { 'chevron-right': CircleGlyph } }}>
<MosaicProvider
appearance={{
// Overrides are elements now, not render functions: Mosaic injects its sizing className and
// `data-cl-slot` into the element via cloneElement, so the replacement only needs its viewBox
// + paths. Passing an element (vs a function) also lets overrides be supplied from a Server
// Component, since elements serialize across the RSC boundary.
icons: {
'chevron-right': (
<svg
viewBox='0 0 20 20'
fill='currentColor'
>
<circle
cx={10}
cy={10}
r={6}
/>
</svg>
),
},
}}
>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<Icon name='chevron-right' />
<Icon name='chevron-left' />
Expand Down
30 changes: 0 additions & 30 deletions packages/swingset/src/stories/leave-organization.stories.tsx

This file was deleted.

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
import * as DeleteOrganizationStories from './delete-organization.stories';
import * as OrganizationProfileDeleteSectionStories from './organization-profile-delete-section.stories';

# Delete Organization
# Organization Profile Delete Section

A section that owns the open/deleting state and wires the `Destructive` block to the delete-organization flow.

<Story
name='Default'
storyModule={DeleteOrganizationStories}
storyModule={OrganizationProfileDeleteSectionStories}
composition={[
{ name: 'Destructive', href: '/blocks/destructive', layer: 'Blocks' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
/** @jsxImportSource @emotion/react */
import { useMachine } from '@clerk/ui/mosaic/machine/useMachine';
import { organizationProfileDeleteSectionMachine } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.machine';
import { OrganizationProfileDeleteSectionView } from '@clerk/ui/mosaic/organization/organization-profile-delete-section.view';

import type { StoryMeta } from '@/lib/types';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileDeleteSection',
source: 'packages/ui/src/mosaic/organization/organization-profile-delete-section.tsx',
};

export function Default() {
const [snapshot, send, actor] = useMachine(organizationProfileDeleteSectionMachine, {
context: {
organizationName: 'Acme Inc',
destroyOrganization: () => new Promise<void>(resolve => setTimeout(resolve, 800)),
},
});

return (
<OrganizationProfileDeleteSectionView
snapshot={snapshot}
send={send}
canSubmit={actor.can({ type: 'CONFIRM' })}
/>
);
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
import * as OrganizationProfileGeneralPanelStories from './organization-profile-general-panel.stories';

# Organization Profile General Panel

The General tab panel of the Organization Profile — composes the organization-level sections shown under "General".

<Story
name='Default'
storyModule={OrganizationProfileGeneralPanelStories}
composition={[
{
name: 'OrganizationProfileLeaveSection',
href: '/organization/organization-profile-leave-section',
layer: 'Organization',
},
{
name: 'OrganizationProfileDeleteSection',
href: '/organization/organization-profile-delete-section',
layer: 'Organization',
},
]}
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/** @jsxImportSource @emotion/react */
import { OrganizationProfileGeneralPanelView } from '@clerk/ui/mosaic/organization/organization-profile-general-panel-view';

import type { StoryMeta } from '@/lib/types';

import { Default as OrganizationProfileDeleteSectionDemo } from './organization-profile-delete-section.stories';
import { Default as OrganizationProfileLeaveSectionDemo } from './organization-profile-leave-section.stories';

export const meta: StoryMeta = {
group: 'Organization',
title: 'OrganizationProfileGeneralPanel',
source: 'packages/ui/src/mosaic/organization/organization-profile-general-panel.tsx',
};

export function Default() {
return (
<OrganizationProfileGeneralPanelView
leaveOrganization={<OrganizationProfileLeaveSectionDemo />}
deleteOrganization={<OrganizationProfileDeleteSectionDemo />}
/>
);
}
14 changes: 0 additions & 14 deletions packages/swingset/src/stories/organization-profile-general.mdx

This file was deleted.

This file was deleted.

Loading
Loading