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
12 changes: 12 additions & 0 deletions .cursor/skills/dev-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Apply when generating or modifying UI code in this repo.
- **Location:** `src/components/<ComponentName>/styling/`.
- **Required:** Each component styling folder has an `index.scss`.
- **Registration:** Each `src/components/<ComponentName>/styling/index.scss` is imported in `src/styling/index.scss` with an alias.
- **Specificity:** Each component has own `.scss` file in the `src/components/<ComponentName>/styling` folder

**Import order in `src/styling/index.scss`:**

Expand All @@ -35,6 +36,17 @@ Apply when generating or modifying UI code in this repo.

Source: `.ai/DEV_PATTERNS.md`.

## Translating quantities (plurals)

- **Use plural suffixes only:** `_one`, `_other`, and `_few`, `_many` where the locale requires them.
- **Do not** add a standalone key (e.g. `"{{count}} new messages"`). Only add quantified variants: `"{{count}} new messages_one"`, `"{{count}} new messages_other"`, etc.
- Follow existing patterns in `src/i18n/` (e.g. `{{count}} unread_one`, `unreadMessagesSeparatorText_other`).
- Locale plural rules (CLDR): `en`, `de`, `nl`, `tr`, `hi`, `ko`, `ja` use `_one` + `_other`; `es`, `fr`, `it`, `pt` add `_many`; `ru` uses `_one`, `_few`, `_many`, `_other`.

## Imports

When importing from 'stream-chat' library, always import by library name (from 'stream-chat'), not relative path (from '..path/to/from 'stream-chat-js/src').

## React components

Try to avoid inline `style` attribute and prefer adding styles to `.scss` files.
1 change: 0 additions & 1 deletion examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ const App = () => {
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
{/*<VirtualizedMessageList />*/}
<AIStateIndicator />
<MessageInput
focus
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/src/stream-imports-layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//@use 'stream-chat-react/dist/scss/v2/AttachmentPreviewList/AttachmentPreviewList-layout'; // X
//@use 'stream-chat-react/dist/scss/v2/Autocomplete/Autocomplete-layout';
//@use 'stream-chat-react/dist/scss/v2/AudioRecorder/AudioRecorder-layout';
@use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-layout';
//@use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-layout';
//@use 'stream-chat-react/dist/scss/v2/Channel/Channel-layout';
@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-layout';
@use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-layout';
Expand Down Expand Up @@ -35,7 +35,7 @@
// @use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactions-layout';
@use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactionsSelector-layout';
//@use 'stream-chat-react/dist/scss/v2/Modal/Modal-layout';
@use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-layout';
//@use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-layout';
@use 'stream-chat-react/dist/scss/v2/Notification/NotificationList-layout';
@use 'stream-chat-react/dist/scss/v2/Notification/Notification-layout';
//@use 'stream-chat-react/dist/scss/v2/Poll/Poll-layout';
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/src/stream-imports-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//@use 'stream-chat-react/dist/scss/v2/AttachmentList/AttachmentList-theme';
//@use 'stream-chat-react/dist/scss/v2/AudioRecorder/AudioRecorder-theme';
//@use 'stream-chat-react/dist/scss/v2/Autocomplete/Autocomplete-theme';
@use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-theme';
//@use 'stream-chat-react/dist/scss/v2/BaseImage/BaseImage-theme';
//@use 'stream-chat-react/dist/scss/v2/Channel/Channel-theme.scss';
@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-theme';
@use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-theme';
Expand All @@ -29,7 +29,7 @@
// @use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactions-theme';
@use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactionsSelector-theme';
//@use 'stream-chat-react/dist/scss/v2/Modal/Modal-theme';
@use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-theme';
//@use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-theme';
@use 'stream-chat-react/dist/scss/v2/Notification/NotificationList-theme';
@use 'stream-chat-react/dist/scss/v2/Notification/Notification-theme';
//@use 'stream-chat-react/dist/scss/v2/Poll/Poll-theme';
Expand Down
1 change: 1 addition & 0 deletions src/components/Attachment/styling/Attachment.scss
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@
/* Right (left in RTL layout) border of audio widget's play / pause button */
--str-chat__audio-attachment-controls-button-border-inline-end: none;

// todo: we need to solve whether we want to keep the CSS variables. E.g. --str-chat__circle-fab-box-shadow is not declared.
/* Box shadow applied to audio widget's play / pause button */
--str-chat__audio-attachment-controls-button-box-shadow: var(
--str-chat__circle-fab-box-shadow
Expand Down
37 changes: 37 additions & 0 deletions src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import clsx from 'clsx';
import React, { type ComponentProps } from 'react';

export type BadgeVariant = 'default' | 'primary' | 'error' | 'neutral' | 'inverse';

export type BadgeSize = 'sm' | 'md' | 'lg';

export type BadgeProps = ComponentProps<'span'> & {
/** Visual variant mapping to design tokens */
variant?: BadgeVariant;
/** Size preset (typography and padding) */
size?: BadgeSize;
};

/**
* Compact pill/circle badge for counts and labels.
* Uses design tokens: --badge-bg-*, --badge-text-*, --badge-border.
*/
export const Badge = ({
children,
className,
size = 'md',
variant = 'default',
...spanProps
}: BadgeProps) => (
<span
{...spanProps}
className={clsx(
'str-chat__badge',
`str-chat__badge--variant-${variant}`,
`str-chat__badge--size-${size}`,
className,
)}
>
{children}
</span>
);
35 changes: 35 additions & 0 deletions src/components/Badge/__tests__/Badge.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import { Badge } from '../Badge';

describe('Badge', () => {
it('renders children', () => {
render(<Badge>17</Badge>);
expect(screen.getByText('17')).toBeInTheDocument();
});

it('applies variant class', () => {
const { container } = render(<Badge variant='primary'>1</Badge>);
expect(container.firstChild).toHaveClass('str-chat__badge--variant-primary');
});

it('applies size class', () => {
const { container } = render(<Badge size='md'>1</Badge>);
expect(container.firstChild).toHaveClass('str-chat__badge--size-md');
});

it('passes data-testid', () => {
render(<Badge data-testid='custom-badge'>99</Badge>);
expect(screen.getByTestId('custom-badge')).toHaveTextContent('99');
});

it('merges className', () => {
const { container } = render(
<Badge className='str-chat__jump-to-latest__unread-count'>5</Badge>,
);
expect(container.firstChild).toHaveClass('str-chat__badge');
expect(container.firstChild).toHaveClass('str-chat__jump-to-latest__unread-count');
});
});
1 change: 1 addition & 0 deletions src/components/Badge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Badge';
71 changes: 71 additions & 0 deletions src/components/Badge/styling/Badge.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Badge: compact pill for counts/labels. Design tokens: badge-bg-*, badge-text-*, badge-border.
// Figma: Badge Notification (scroll-to-bottom unread count)

.str-chat__badge {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: var(--typography-font-weight-bold);
border-radius: var(--radius-max);
line-height: 1;
border-style: solid;
}

// Variants: map to design tokens
.str-chat__badge--variant-default {
background: var(--badge-bg-default);
color: var(--badge-text);
border-color: var(--badge-border);
}

.str-chat__badge--variant-primary {
background: var(--badge-bg-primary);
color: var(--badge-text-on-accent);
border-color: var(--badge-border);
}

.str-chat__badge--variant-error {
background: var(--badge-bg-error);
color: var(--badge-text-on-accent);
border-color: var(--badge-border);
}

.str-chat__badge--variant-neutral {
background: var(--badge-bg-neutral);
color: var(--badge-text-on-accent);
border-color: var(--badge-border);
}

.str-chat__badge--variant-inverse {
background: var(--badge-bg-inverse);
color: var(--badge-text-inverse);
border-color: var(--badge-border);
}

// Sizes: caption-numeric equivalents (sm/12px, md/14px, lg/16px)
.str-chat__badge--size-sm {
font-size: var(--typography-font-size-xxs);
min-width: 16px;
min-height: 16px;
padding-inline: var(--spacing-xxxs);
border-width: 1px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
}

.str-chat__badge--size-md {
font-size: var(--typography-font-size-xs);
min-width: 20px;
min-height: 20px;
padding-inline: var(--spacing-xxs);
border-width: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
}

.str-chat__badge--size-lg {
font-size: var(--typography-font-size-sm);
min-width: 24px;
min-height: 24px;
padding-inline: var(--spacing-xs);
border-width: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
}
1 change: 1 addition & 0 deletions src/components/Badge/styling/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@use 'Badge';
6 changes: 5 additions & 1 deletion src/components/Button/styling/Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@include utils.button-reset;
position: relative; /* creates positioning context for pseudo ::after overlay */
overflow: hidden;

white-space: nowrap;
cursor: pointer;

display: flex;
Expand Down Expand Up @@ -111,6 +111,10 @@
box-shadow: var(--light-elevation-2);
}

&::after {
border-radius: inherit;
}

&.str-chat__button--size-lg {
padding-block: var(--button-padding-y-lg);
padding-inline: var(--button-padding-x-with-label-lg);
Expand Down
1 change: 1 addition & 0 deletions src/components/Button/styling/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@use 'Button';
17 changes: 16 additions & 1 deletion src/components/DateSeparator/DateSeparator.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';

import { useTranslationContext } from '../../context/TranslationContext';
Expand All @@ -6,8 +7,12 @@ import { getDateString } from '../../i18n/utils';
import type { TimestampFormatterOptions } from '../../i18n/types';

export type DateSeparatorProps = TimestampFormatterOptions & {
/** Optional className for the root element */
className?: string;
/** The date to format */
date: Date;
/** When true, applies floating positioning (fixed at top when scrolling) */
floating?: boolean;
/** Override the default formatting of the date. This is a function that has access to the original date object. */
formatDate?: (date: Date) => string;
// todo: position and unread are not necessary anymore
Expand All @@ -20,7 +25,9 @@ export type DateSeparatorProps = TimestampFormatterOptions & {
const UnMemoizedDateSeparator = (props: DateSeparatorProps) => {
const {
calendar,
className,
date: messageCreatedAt,
floating,
formatDate,
...restTimestampFormatterOptions
} = props;
Expand All @@ -38,7 +45,15 @@ const UnMemoizedDateSeparator = (props: DateSeparatorProps) => {
});

return (
<div className='str-chat__date-separator' data-testid='date-separator'>
<div
className={clsx(
'str-chat__date-separator',
{ 'str-chat__date-separator--floating': floating },
className,
)}
data-date={messageCreatedAt.toISOString()}
data-testid={floating ? 'floating-date-separator' : 'date-separator'}
>
<div className='str-chat__date-separator-date'>{formattedDate}</div>
</div>
);
Expand Down
11 changes: 11 additions & 0 deletions src/components/DateSeparator/styling/DateSeparator.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
--str-chat__date-separator-box-shadow: none;
}

.str-chat__date-separator--floating {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
display: flex;
justify-content: center;
pointer-events: none;
}

.str-chat__date-separator {
@include utils.component-layer-overrides('date-separator');
display: flex;
Expand Down
35 changes: 0 additions & 35 deletions src/components/Message/styling/UnreadMessageNotification.scss

This file was deleted.

15 changes: 0 additions & 15 deletions src/components/Message/styling/UnreadMessagesSeparator.scss

This file was deleted.

2 changes: 0 additions & 2 deletions src/components/Message/styling/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@
@use 'MessageTranslationIndicator';
@use 'QuotedMessage';
@use 'ReminderNotification';
@use 'UnreadMessageNotification';
@use 'UnreadMessagesSeparator';
@use 'MessageRepliesCountButton';
Loading
Loading