Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
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): Mosaic dialog component by alexcarpenter · Pull Request #8884 · clerk/javascript · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tiny-coats-wear.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,6 +181,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.click(screen.getByText('Banana'));
Expand DownExpand Up@@ -241,6 +242,7 @@ describe('Autocomplete', () => {
const user = userEvent.setup();
render(<FilteredAutocomplete />);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search fruits...') as HTMLInputElement;
await user.type(input, 'b');
await user.keyboard('{Enter}');
Expand DownExpand Up@@ -667,10 +669,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -812,10 +811,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List style={{ maxHeight: 80, overflowY: 'auto' }}>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -905,10 +901,7 @@ describe('Autocomplete', () => {
setPopoverOpen(false);
}}
>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{filtered.map(f => (
<Autocomplete.Option
Expand DownExpand Up@@ -948,6 +941,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Pick a fruit...'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand All@@ -962,6 +956,7 @@ describe('Autocomplete', () => {

await user.click(screen.getByText('Cherry'));

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const input = screen.getByPlaceholderText('Search...') as HTMLInputElement;
expect(document.activeElement).toBe(input);
expect(input.value).toBe('');
Expand DownExpand Up@@ -1218,10 +1213,7 @@ describe('Autocomplete', () => {
<Popover.Popup>
<Popover.Title>Fruit picker</Popover.Title>
<Autocomplete.Root open>
<Autocomplete.Input
placeholder='Search...'
autoFocus
/>
<Autocomplete.Input placeholder='Search...' />
<Autocomplete.List>
{fruits.map(f => (
<Autocomplete.Option
Expand Down
7 changes: 6 additions & 1 deletion packages/headless/src/primitives/dialog/dialog.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -223,7 +223,12 @@ describe('Dialog', () => {
const user = userEvent.setup();
render(
<>
<button onClick={onBackgroundClick}>Background button</button>
<button
type='button'
onClick={onBackgroundClick}
>
Background button
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger>Open dialog</Dialog.Trigger>
<Dialog.Backdrop />
Expand Down
3 changes: 3 additions & 0 deletions packages/headless/src/primitives/dialog/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
export * as Dialog from './parts';

export { useDialogContext } from './dialog-context';
export type { DialogContextValue } from './dialog-context';

export type {
DialogBackdropProps,
DialogCloseProps,
Expand Down
5 changes: 3 additions & 2 deletions packages/headless/src/primitives/select/select-option.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,9 +24,10 @@ export function SelectOption(props: SelectOptionProps) {
const isActive = activeIndex === index;

useEffect(() => {
valueToLabelRef.current.set(value, displayLabel);
const map = valueToLabelRef.current;
map.set(value, displayLabel);
return () => {
valueToLabelRef.current.delete(value);
map.delete(value);
};
}, [value, displayLabel, valueToLabelRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/headless/src/utils/css-vars.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,13 +58,13 @@ function createMockState(
platform: {
getElementRects: vi.fn(),
getDimensions: vi.fn(),
getClippingRect: vi.fn(async () => ({
getClippingRect: vi.fn(() => ({
width: 1024,
height: 768,
x: 0,
y: 0,
})),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(async ({ rect }: { rect: unknown }) => rect),
convertOffsetParentRelativeRectToViewportRelativeRect: vi.fn(({ rect }: { rect: unknown }) => rect),
},
x: 0,
y: 0,
Expand Down
12 changes: 12 additions & 0 deletions packages/headless/src/utils/render-element.test-d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type React from 'react';
import { describe, expectTypeOf, test } from 'vitest';

import type { ComponentProps } from './render-element';

describe('render-element', () => {
test('render prop arg is narrowed to the element tag props, not the generic HTMLAttributes<HTMLElement>', () => {
type Props = ComponentProps<'button'>;
type RenderArg = NonNullable<Props['render']> extends (props: infer P) => React.ReactElement ? P : never;
expectTypeOf<RenderArg>().toEqualTypeOf<React.ComponentPropsWithRef<'button'>>();
});
});
10 changes: 5 additions & 5 deletions packages/headless/src/utils/render-element.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ export type RenderProp<Props = React.HTMLAttributes<HTMLElement>> = (props: Prop

/**
* Props accepted by any primitive part. Extends the native props for `Tag`
* and adds the optional `render` escape hatch.
* and adds the optional `render` escape hatch, narrowed to that tag's props.
*/
export type ComponentProps<Tag extends keyof React.JSX.IntrinsicElements> = React.ComponentPropsWithRef<Tag> & {
render?: RenderProp;
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
};

/**
Expand DownExpand Up@@ -98,8 +98,8 @@ interface RenderElementParamsBase<
> {
/** Fallback HTML tag when `render` is not provided. */
defaultTagName: Tag;
/** Render prop from the consumer. */
render?: RenderProp;
/** Render prop from the consumer, narrowed to the element's native props. */
render?: RenderProp<React.ComponentPropsWithRef<Tag>>;
/** State object. Keys are mapped to data attributes via `stateAttributesMapping`. */
state?: State;
/** Custom mapping from state keys to data-attribute objects. */
Expand DownExpand Up@@ -165,7 +165,7 @@ export function renderElement<
const computedProps = { ...props, ...dataAttrs };

if (render) {
return render(computedProps as React.HTMLAttributes<HTMLElement>);
return render(computedProps as React.ComponentPropsWithRef<Tag>);
}

return React.createElement(defaultTagName, computedProps);
Expand Down
2 changes: 2 additions & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,9 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
button: dynamic(() => import('../stories/button.mdx')),
input: dynamic(() => import('../stories/input.mdx')),
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
tabs: dynamic(() => import('../stories/tabs.component.mdx')),
text: dynamic(() => import('../stories/text.mdx')),
},
primitives: {
// Headless primitives — alphabetical.
Expand Down
23 changes: 23 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,12 @@ import {
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';
import {
Default as HeadingDefault,
Intents as HeadingIntents,
meta as headingMeta,
Sizes as HeadingSizes,
} from '../stories/heading.stories';
import {
Default,
Disabled as InputDisabled,
Expand All@@ -34,6 +40,12 @@ import { meta as popoverMeta } from '../stories/popover.stories';
import { meta as selectMeta } from '../stories/select.stories';
import { Default as TabsComponentDefault, meta as tabsComponentMeta } from '../stories/tabs.component.stories';
import { meta as tabsMeta } from '../stories/tabs.stories';
import {
Default as TextDefault,
Intents as TextIntents,
meta as textMeta,
Sizes as TextSizes,
} from '../stories/text.stories';
import { meta as tooltipMeta } from '../stories/tooltip.stories';
import { toSlug } from './slug';
import type { StoryModule } from './types';
Expand All@@ -53,8 +65,17 @@ const inputModule: StoryModule = { meta: inputMeta, Default, Sizes: InputSizes,

const dialogComponentModule: StoryModule = { meta: dialogComponentMeta, Default: DialogDefault };

const headingModule: StoryModule = {
meta: headingMeta,
Default: HeadingDefault,
Sizes: HeadingSizes,
Intents: HeadingIntents,
};

const tabsComponentModule: StoryModule = { meta: tabsComponentMeta, Default: TabsComponentDefault };

const textModule: StoryModule = { meta: textMeta, Default: TextDefault, Sizes: TextSizes, Intents: TextIntents };

// Headless primitives carry just `meta` (no story functions). Like every component
// they're documented as a single overview page; their live demos come from `<Story>` /
// `<Preview>` embeds in the MDX, which import the stories module directly.
Expand DownExpand Up@@ -82,7 +103,9 @@ export const registry: StoryModule[] = [
buttonModule,
inputModule,
dialogComponentModule,
headingModule,
tabsComponentModule,
textModule,
// Primitives — alphabetical within the group.
accordionModule,
autocompleteModule,
Expand Down
11 changes: 8 additions & 3 deletions packages/swingset/src/stories/button.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import * as ButtonStories from './button.stories';

# Button

The `Button` component is the primary action element in Mosaic. It supports color and size variants and uses Emotion CSS-in-JS for styling via the Mosaic CVA utility.
Buttonis the primary action element in Mosaic, used for form submissions and discrete user-triggered actions.

## Playground

Expand All@@ -17,8 +17,6 @@ The `Button` component is the primary action element in Mosaic. It supports colo

## Usage

The snippet below reflects the props selected in the table above — change a prop and it updates here.

<Usage
component='Button'
module='@clerk/ui/mosaic/components/button'
Expand All@@ -37,6 +35,13 @@ The snippet below reflects the props selected in the table above — change a pr
storyModule={ButtonStories}
/>

### Shapes

<Story
name='Shapes'
storyModule={ButtonStories}
/>

### Disabled

<Story
Expand Down
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/button.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,6 +40,93 @@ export function Sizes(props: Record<string, unknown>) {
);
}

export function Shapes(props: Record<string, unknown>) {
return (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Button
{...knobsAsProps(props)}
shape='square'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='square'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='sm'
aria-label='Add'
>
<svg
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
<Button
{...knobsAsProps(props)}
shape='circle'
size='md'
aria-label='Add'
>
<svg
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
style={{ flexShrink: 0 }}
>
<path d='M12 5v14M5 12h14' />
</svg>
</Button>
</div>
);
}

export function Disabled(props: Record<string, unknown>) {
return (
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/swingset/src/stories/destructive.stories.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,11 +12,11 @@ export const meta: StoryMeta = {
source: 'packages/ui/src/mosaic/block/destructive.tsx',
};

function DestructiveTrigger(props: Omit<HTMLAttributes<HTMLElement>, 'color'>) {
function DestructiveTrigger(props: HTMLAttributes<HTMLElement>) {
return (
<Button
{...props}
color='destructive'
intent='destructive'
>
Delete organization
</Button>
Expand Down
Loading
Loading