Skip to content
Draft
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
18 changes: 18 additions & 0 deletions src/components/Item/Item.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@
opacity: calc(var(--opacity-50) / 100);
pointer-events: none;
}

// Compact density overrides
&[data-size='compact'] {
padding-block: var(--spacing-xs);
padding-inline-start: var(--spacing-md);

.content {
gap: var(--spacing-px);
}

.title {
@include body;
}

.description {
@include body-sm;
}
}
}

.clickable {
Expand Down
42 changes: 42 additions & 0 deletions src/components/Item/Item.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const meta: Meta<typeof Item> = {
argTypes: {
title: { control: 'text' },
description: { control: 'text' },
size: {
control: 'radio',
options: ['default', 'compact'],
},
clickable: { control: 'boolean' },
selected: { control: 'boolean' },
disabled: { control: 'boolean' },
Expand Down Expand Up @@ -88,3 +92,41 @@ function SelectableListComponent() {
export const SelectableList: Story = {
render: () => <SelectableListComponent />,
};

const transactions = [
{ id: 'tx-1', title: 'Payment received', description: '2 minutes ago', amount: '+$120.00' },
{ id: 'tx-2', title: 'Invoice paid', description: '1 hour ago', amount: '-$45.50' },
{ id: 'tx-3', title: 'Transfer sent', description: 'Yesterday', amount: '-$300.00' },
];

function SizeComparisonComponent() {
return (
<div style={{ display: 'flex', gap: 'var(--spacing-2xl)' }}>
{(['default', 'compact'] as const).map((size) => (
<div key={size} style={{ display: 'flex', flexDirection: 'column', width: 320 }}>
{transactions.map((transaction) => (
<Item
key={transaction.id}
size={size}
title={transaction.title}
description={transaction.description}
trailing={<span>{transaction.amount}</span>}
/>
))}
</div>
))}
</div>
);
}

export const SizeComparison: Story = {
parameters: {
docs: {
description: {
story:
'Default and compact densities side by side. Compact tightens padding, gap, and type scale for dense lists such as transaction rows.',
},
},
},
render: () => <SizeComparisonComponent />,
};
9 changes: 9 additions & 0 deletions src/components/Item/Item.test-stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ export function NonClickableItem() {
return <Item title="Static item" clickable={false} />;
}

// Sizes
export function CompactItem() {
return <Item title="Payment received" description="2 minutes ago" size="compact" />;
}

export function DefaultSizeItem() {
return <Item title="Payment received" description="2 minutes ago" />;
}

// States
export function SelectedItem() {
return (
Expand Down
15 changes: 15 additions & 0 deletions src/components/Item/Item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
ItemWithLeading,
ItemWithTrailing,
ItemWithBothSlots,
CompactItem,
DefaultSizeItem,
ClickableItem,
NonClickableItem,
SelectedItem,
Expand Down Expand Up @@ -49,6 +51,19 @@ test.describe('Item', () => {
});
});

test.describe('Sizes', () => {
test('compact item has data-size="compact"', async ({ mount, page }) => {
await mount(<CompactItem />);
await expect(page.locator('[data-size="compact"]')).toBeVisible();
});

test('default item has no data-size attribute', async ({ mount, page }) => {
await mount(<DefaultSizeItem />);
await expect(page.getByText('Payment received')).toBeVisible();
await expect(page.locator('[data-size]')).toHaveCount(0);
});
});

test.describe('Clickable behavior', () => {
test('clickable item responds to click', async ({ mount, page }) => {
await mount(<ClickableItem />);
Expand Down
6 changes: 6 additions & 0 deletions src/components/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import * as React from 'react';
import clsx from 'clsx';
import styles from './Item.module.scss';

export type ItemSize = 'default' | 'compact';

export interface ItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> {
title: string;
description?: string;
leading?: React.ReactNode;
trailing?: React.ReactNode;
trailingPadding?: 'sm' | 'lg';
/** Item density — compact reduces padding and type scale for dense lists */
size?: ItemSize;
clickable?: boolean;
selected?: boolean;
disabled?: boolean;
Expand All @@ -24,6 +28,7 @@ export const Item = React.forwardRef<HTMLDivElement, ItemProps>(
leading,
trailing,
trailingPadding,
size = 'default',
clickable = true,
selected,
disabled,
Expand Down Expand Up @@ -51,6 +56,7 @@ export const Item = React.forwardRef<HTMLDivElement, ItemProps>(
const itemProps = {
ref: forwardedRef,
className: clsx(styles.root, clickable && styles.clickable, className),
'data-size': size !== 'default' ? size : undefined,
'data-clickable': clickable ? undefined : 'false',
'data-selected': selected || undefined,
'data-disabled': disabled || undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Item/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { Item } from './Item';
export type { ItemProps } from './Item';
export type { ItemProps, ItemSize } from './Item';
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export type { CentralIconName, CentralIconProps } from './components/Icon';

export { Input, type InputProps } from './components/Input';

export { Item, type ItemProps } from './components/Item';
export { Item, type ItemProps, type ItemSize } from './components/Item';

export { Logo } from './components/Logo';
export type { LogoProps } from './components/Logo';
Expand Down
Loading