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/mosaic-entity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
alexcarpenter marked this conversation as resolved.
55 changes: 55 additions & 0 deletions packages/mosaic/src/components/entity/entity.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as stylex from '@stylexjs/stylex';

import { colorVars, fontFamilyVars, fontWeightVars, space, typeScaleVars } from '../../tokens.stylex';

export const entity = stylex.create({
base: {
gap: space['2'],
alignItems: 'center',
display: 'flex',
fontFamily: fontFamilyVars['--cl-font-family-sans'],
minWidth: 0,
},
});

export const media = stylex.create({
base: {
alignItems: 'center',
aspectRatio: '1/1',
display: 'flex',
flexShrink: 0,
justifyContent: 'center',
width: space['9.5'],
},
});

export const content = stylex.create({
base: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minWidth: 0,
},
});

export const label = stylex.create({
base: {
gap: space['2'],
alignItems: 'center',
color: colorVars['--cl-color-foreground'],
display: 'flex',
fontSize: typeScaleVars['--cl-text-sm-size'],
fontWeight: fontWeightVars['--cl-font-medium'],
lineHeight: typeScaleVars['--cl-text-sm-leading'],
minWidth: 0,
},
});

export const description = stylex.create({
base: {
color: colorVars['--cl-color-foreground-secondary'],
fontSize: typeScaleVars['--cl-text-xs-size'],
fontWeight: fontWeightVars['--cl-font-normal'],
lineHeight: typeScaleVars['--cl-text-xs-leading'],
},
});
63 changes: 63 additions & 0 deletions packages/mosaic/src/components/entity/entity.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as stylex from '@stylexjs/stylex';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { describe, expect, it } from 'vitest';

import { Entity } from './entity';

const overrides = stylex.create({
root: { marginTop: '8px' },
});

const atoms = (style: stylex.StyleXStyles) =>
(stylex.props(style).className ?? '').split(' ').filter(name => /^x[a-z0-9]+$/.test(name));

describe('Mosaic Entity', () => {
it('renders the composed parts with their stable classes', () => {
render(
<Entity.Root data-testid='root'>
<Entity.Media data-testid='media'>
<span>avatar</span>
</Entity.Media>
<Entity.Content data-testid='content'>
<Entity.Label data-testid='label'>Cameron Walker</Entity.Label>
<Entity.Description>cameron@clerk.com</Entity.Description>
</Entity.Content>
</Entity.Root>,
);
expect(screen.getByTestId('root')).toHaveClass('cl-entity');
expect(screen.getByTestId('media')).toHaveClass('cl-entity-media');
expect(screen.getByTestId('content')).toHaveClass('cl-entity-content');
expect(screen.getByTestId('label')).toHaveClass('cl-entity-label');
expect(screen.getByText('cameron@clerk.com')).toHaveClass('cl-entity-description');
});

it('gives the label text its own box to truncate, leaving a badge beside it whole', () => {
render(
<Entity.Label data-testid='label'>
Cameron Walker
<span>Default</span>
</Entity.Label>,
);
const label = screen.getByTestId('label');
expect(screen.getByText('Cameron Walker')).not.toBe(label);
expect(screen.getByText('Cameron Walker').parentElement).toBe(label);
expect(screen.getByText('Default').parentElement).toBe(label);
});

it('applies xstyle to the root element', () => {
render(<Entity.Root xstyle={overrides.root}>Hi</Entity.Root>);
expect(screen.getByText('Hi')).toHaveClass('cl-entity', ...atoms(overrides.root));
});

it('merges a render-sourced className instead of clobbering its own', () => {
render(<Entity.Content render={<Entity.Description />}>Hi</Entity.Content>);
expect(screen.getByText('Hi')).toHaveClass('cl-entity-content', 'cl-entity-description');
});

it('forwards the ref to the root element', () => {
const ref = React.createRef<HTMLDivElement>();
render(<Entity.Root ref={ref}>Hi</Entity.Root>);
expect(ref.current).toBe(screen.getByText('Hi'));
});
});
93 changes: 93 additions & 0 deletions packages/mosaic/src/components/entity/entity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { useRender } from '../../primitives/utils';
import type { MosaicComponentProps } from '../../props';
import { mergeStyleProps, themeProps } from '../../props';
import { reset } from '../../styles/reset.styles';
import { truncationStyles } from '../../styles/typography.styles';
import { withTruncatableLabel } from '../../utils/truncatable-label';
import * as slots from './entity.styles';

const Root = React.forwardRef<HTMLDivElement, MosaicComponentProps<'div'>>(function MosaicEntity(
Comment thread
alexcarpenter marked this conversation as resolved.
{ render, xstyle, ...rest },
ref,
) {
return useRender({
defaultTagName: 'div',
render,
ref,
props: {
...mergeStyleProps(themeProps('entity'), stylex.props(reset.base, slots.entity.base, xstyle), rest),
},
});
});

const Media = React.forwardRef<HTMLDivElement, MosaicComponentProps<'div'>>(function MosaicEntityMedia(
{ render, xstyle, ...rest },
ref,
) {
return useRender({
defaultTagName: 'div',
render,
ref,
props: {
...mergeStyleProps(themeProps('entity-media'), stylex.props(reset.base, slots.media.base, xstyle), rest),
},
});
});

const Content = React.forwardRef<HTMLDivElement, MosaicComponentProps<'div'>>(function MosaicEntityContent(
{ render, xstyle, ...rest },
ref,
) {
return useRender({
defaultTagName: 'div',
render,
ref,
props: {
...mergeStyleProps(themeProps('entity-content'), stylex.props(reset.base, slots.content.base, xstyle), rest),
},
});
});

const Label = React.forwardRef<HTMLDivElement, MosaicComponentProps<'div'>>(function MosaicEntityLabel(
{ children, render, xstyle, ...rest },
ref,
) {
return useRender({
defaultTagName: 'div',
render,
ref,
props: {
...mergeStyleProps(themeProps('entity-label'), stylex.props(reset.base, slots.label.base, xstyle), rest),
children: withTruncatableLabel(children),
},
});
});

const Description = React.forwardRef<HTMLDivElement, MosaicComponentProps<'div'>>(function MosaicEntityDescription(
{ render, xstyle, ...rest },
ref,
) {
return useRender({
defaultTagName: 'div',
render,
ref,
props: {
...mergeStyleProps(
themeProps('entity-description'),
stylex.props(reset.base, slots.description.base, truncationStyles.singleLine, xstyle),
rest,
),
},
});
});

export const Entity = {
Root,
Media,
Content,
Label,
Description,
};
1 change: 1 addition & 0 deletions packages/mosaic/src/components/entity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Entity } from './entity';
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import * as stylex from '@stylexjs/stylex';

import { space } from '../../tokens.stylex';

export const styles = stylex.create({
deprovisionedRow: { opacity: 0.5 },
name: { gap: space['2'], alignItems: 'center', display: 'flex' },
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Badge } from '../../components/badge';
import { Banner } from '../../components/banner';
import { Button } from '../../components/button';
import { EmptyState } from '../../components/empty-state';
import { Entity } from '../../components/entity';
import { Icon } from '../../components/icon';
import { Item } from '../../components/item';
import { Pagination } from '../../components/pagination';
import { Select } from '../../components/select';
import { Spinner } from '../../components/spinner';
Expand Down Expand Up @@ -317,8 +317,8 @@ function MemberRow({
/>
) : null}
<Table.Cell>
<Item.Root>
<Item.Media>
<Entity.Root>
<Entity.Media>
<Avatar.Root
size='fit'
aria-hidden
Expand All @@ -331,9 +331,9 @@ function MemberRow({
) : null}
<Avatar.Fallback />
</Avatar.Root>
</Item.Media>
<Item.Content>
<Item.Label xstyle={styles.name}>
</Entity.Media>
<Entity.Content>
<Entity.Label>
{member.name}
{member.isCurrentUser ? (
<Badge>{m.you}</Badge>
Expand All @@ -342,10 +342,10 @@ function MemberRow({
) : member.isBanned ? (
<Badge color='negative'>{m.banned}</Badge>
) : null}
</Item.Label>
<Item.Description>{member.email}</Item.Description>
</Item.Content>
</Item.Root>
</Entity.Label>
<Entity.Description>{member.email}</Entity.Description>
</Entity.Content>
</Entity.Root>
</Table.Cell>
<Table.Cell noWrap>{member.joinedAtLabel}</Table.Cell>
<Table.Cell>
Expand Down
1 change: 1 addition & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
dialog: dynamic(() => import('../stories/dialog.component.mdx')),
drawer: dynamic(() => import('../stories/drawer.component.mdx')),
'empty-state': dynamic(() => import('../stories/empty-state.mdx')),
entity: dynamic(() => import('../stories/entity.mdx')),
heading: dynamic(() => import('../stories/heading.mdx')),
icon: dynamic(() => import('../stories/icon.mdx')),
'icon-frame': dynamic(() => import('../stories/icon-frame.mdx')),
Expand Down
16 changes: 16 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ import {
meta as emptyStateMeta,
NoResults as EmptyStateNoResults,
} from '../stories/empty-state.stories';
import {
Default as EntityDefault,
LabelOnly as EntityLabelOnly,
meta as entityMeta,
Truncation as EntityTruncation,
WithBadge as EntityWithBadge,
} from '../stories/entity.stories';
import {
Default as FieldDefault,
meta as fieldMeta,
Expand Down Expand Up @@ -593,6 +600,14 @@ const dataListModule: StoryModule = {
Plain: DataListPlain,
};

const entityModule: StoryModule = {
meta: entityMeta,
Default: EntityDefault,
WithBadge: EntityWithBadge,
LabelOnly: EntityLabelOnly,
Truncation: EntityTruncation,
};

const itemModule: StoryModule = {
meta: itemMeta,
Default: ItemDefault,
Expand Down Expand Up @@ -994,6 +1009,7 @@ export const registry: StoryModule[] = [
dialogComponentModule,
drawerComponentModule,
emptyStateModule,
entityModule,
headingModule,
iconModule,
iconFrameModule,
Expand Down
Loading
Loading