@linktr.ee/messaging-react 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/components/ActionButton/ActionButton.stories.tsx +46 -0
- package/src/components/ActionButton/ActionButton.test.tsx +112 -0
- package/src/components/ActionButton/index.tsx +33 -0
- package/src/components/Avatar/Avatar.stories.tsx +144 -0
- package/src/components/Avatar/avatarColors.ts +36 -0
- package/src/components/Avatar/index.tsx +64 -0
- package/src/components/ChannelList/ChannelList.stories.tsx +48 -0
- package/src/components/ChannelList/CustomChannelPreview.stories.tsx +303 -0
- package/src/components/ChannelList/CustomChannelPreview.tsx +114 -0
- package/src/components/ChannelList/index.tsx +129 -0
- package/src/components/ChannelView.tsx +422 -0
- package/src/components/CloseButton/index.tsx +16 -0
- package/src/components/IconButton/IconButton.stories.tsx +40 -0
- package/src/components/IconButton/index.tsx +32 -0
- package/src/components/Loading/Loading.stories.tsx +24 -0
- package/src/components/Loading/index.tsx +50 -0
- package/src/components/MessagingShell/EmptyState.stories.tsx +38 -0
- package/src/components/MessagingShell/EmptyState.tsx +55 -0
- package/src/components/MessagingShell/ErrorState.stories.tsx +42 -0
- package/src/components/MessagingShell/ErrorState.tsx +33 -0
- package/src/components/MessagingShell/LoadingState.stories.tsx +26 -0
- package/src/components/MessagingShell/LoadingState.tsx +15 -0
- package/src/components/MessagingShell/index.tsx +298 -0
- package/src/components/ParticipantPicker/ParticipantItem.stories.tsx +188 -0
- package/src/components/ParticipantPicker/ParticipantItem.tsx +59 -0
- package/src/components/ParticipantPicker/ParticipantPicker.stories.tsx +54 -0
- package/src/components/ParticipantPicker/ParticipantPicker.tsx +196 -0
- package/src/components/ParticipantPicker/index.tsx +234 -0
- package/src/components/SearchInput/SearchInput.stories.tsx +33 -0
- package/src/components/SearchInput/SearchInput.test.tsx +108 -0
- package/src/components/SearchInput/index.tsx +50 -0
- package/src/hooks/useMessaging.ts +9 -0
- package/src/hooks/useParticipants.ts +92 -0
- package/src/index.ts +26 -0
- package/src/providers/MessagingProvider.tsx +282 -0
- package/src/stories/mocks.tsx +157 -0
- package/src/test/setup.ts +30 -0
- package/src/test/utils.tsx +23 -0
- package/src/types.ts +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"license": "UNLICENSED",
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"build": "vite build",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import ActionButton from '.'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type ComponentProps = React.ComponentProps<typeof ActionButton>
|
|
7
|
+
|
|
8
|
+
const meta: Meta<ComponentProps> = {
|
|
9
|
+
title: 'ActionButton',
|
|
10
|
+
component: ActionButton,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
onClick: { action: 'clicked' },
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
export default meta
|
|
19
|
+
|
|
20
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
21
|
+
return (
|
|
22
|
+
<div className="p-12">
|
|
23
|
+
<ActionButton {...args} />
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
29
|
+
Default.args = {
|
|
30
|
+
children: 'Default Button',
|
|
31
|
+
variant: 'default',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Danger: StoryFn<ComponentProps> = Template.bind({})
|
|
35
|
+
Danger.args = {
|
|
36
|
+
children: 'Delete',
|
|
37
|
+
variant: 'danger',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Disabled: StoryFn<ComponentProps> = Template.bind({})
|
|
41
|
+
Disabled.args = {
|
|
42
|
+
children: 'Disabled Button',
|
|
43
|
+
variant: 'default',
|
|
44
|
+
disabled: true,
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { renderWithProviders, screen, userEvent } from '../../test/utils';
|
|
3
|
+
import ActionButton from '.';
|
|
4
|
+
|
|
5
|
+
describe('ActionButton', () => {
|
|
6
|
+
describe('Rendering', () => {
|
|
7
|
+
it('renders with children text', () => {
|
|
8
|
+
renderWithProviders(<ActionButton>Click me</ActionButton>);
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders with default variant', () => {
|
|
13
|
+
renderWithProviders(<ActionButton>Default</ActionButton>);
|
|
14
|
+
const button = screen.getByRole('button');
|
|
15
|
+
expect(button).toHaveClass('text-charcoal');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders with danger variant', () => {
|
|
19
|
+
renderWithProviders(<ActionButton variant="danger">Delete</ActionButton>);
|
|
20
|
+
const button = screen.getByRole('button');
|
|
21
|
+
expect(button).toHaveClass('text-danger');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('has full width by default', () => {
|
|
25
|
+
renderWithProviders(<ActionButton>Full Width</ActionButton>);
|
|
26
|
+
const button = screen.getByRole('button');
|
|
27
|
+
expect(button).toHaveClass('w-full');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Disabled State', () => {
|
|
32
|
+
it('is disabled when disabled prop is true', () => {
|
|
33
|
+
renderWithProviders(<ActionButton disabled>Disabled</ActionButton>);
|
|
34
|
+
const button = screen.getByRole('button');
|
|
35
|
+
expect(button).toBeDisabled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('does not trigger onClick when disabled', async () => {
|
|
39
|
+
const handleClick = vi.fn();
|
|
40
|
+
const user = userEvent.setup();
|
|
41
|
+
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<ActionButton disabled onClick={handleClick}>
|
|
44
|
+
Disabled
|
|
45
|
+
</ActionButton>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const button = screen.getByRole('button');
|
|
49
|
+
await user.click(button);
|
|
50
|
+
|
|
51
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('applies disabled styles', () => {
|
|
55
|
+
renderWithProviders(<ActionButton disabled>Disabled</ActionButton>);
|
|
56
|
+
const button = screen.getByRole('button');
|
|
57
|
+
expect(button).toHaveClass('disabled:opacity-60');
|
|
58
|
+
expect(button).toHaveClass('disabled:cursor-not-allowed');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('Click Handling', () => {
|
|
63
|
+
it('calls onClick when clicked', async () => {
|
|
64
|
+
const handleClick = vi.fn();
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
|
|
67
|
+
renderWithProviders(<ActionButton onClick={handleClick}>Click me</ActionButton>);
|
|
68
|
+
|
|
69
|
+
const button = screen.getByRole('button');
|
|
70
|
+
await user.click(button);
|
|
71
|
+
|
|
72
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Type Attribute', () => {
|
|
77
|
+
it('has button type by default', () => {
|
|
78
|
+
renderWithProviders(<ActionButton>Default Type</ActionButton>);
|
|
79
|
+
const button = screen.getByRole('button');
|
|
80
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('can have submit type', () => {
|
|
84
|
+
renderWithProviders(<ActionButton type="submit">Submit</ActionButton>);
|
|
85
|
+
const button = screen.getByRole('button');
|
|
86
|
+
expect(button).toHaveAttribute('type', 'submit');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Accessibility', () => {
|
|
91
|
+
it('is keyboard accessible', async () => {
|
|
92
|
+
const handleClick = vi.fn();
|
|
93
|
+
const user = userEvent.setup();
|
|
94
|
+
|
|
95
|
+
renderWithProviders(<ActionButton onClick={handleClick}>Press me</ActionButton>);
|
|
96
|
+
|
|
97
|
+
const button = screen.getByRole('button');
|
|
98
|
+
button.focus();
|
|
99
|
+
await user.keyboard('{Enter}');
|
|
100
|
+
|
|
101
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('can be focused', () => {
|
|
105
|
+
renderWithProviders(<ActionButton>Focus me</ActionButton>);
|
|
106
|
+
const button = screen.getByRole('button');
|
|
107
|
+
button.focus();
|
|
108
|
+
expect(button).toHaveFocus();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
interface ActionButtonProps
|
|
5
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
variant?: "default" | "danger";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ActionButton = ({
|
|
10
|
+
variant = "default",
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
...rest
|
|
14
|
+
}: ActionButtonProps) => {
|
|
15
|
+
const isDanger = variant === "danger";
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
className={classNames(
|
|
20
|
+
"flex w-full items-center gap-3 rounded-lg px-4 py-3 text-left text-sm transition-colors focus-ring disabled:cursor-not-allowed disabled:opacity-60",
|
|
21
|
+
isDanger
|
|
22
|
+
? "text-danger hover:bg-danger/50"
|
|
23
|
+
: "text-charcoal hover:bg-sand",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...rest}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</button>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default ActionButton;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import { Avatar } from './index'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
type ComponentProps = React.ComponentProps<typeof Avatar>
|
|
6
|
+
|
|
7
|
+
const meta: Meta<ComponentProps> = {
|
|
8
|
+
title: 'Avatar',
|
|
9
|
+
component: Avatar,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'centered'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export default meta
|
|
15
|
+
|
|
16
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className="p-12">
|
|
19
|
+
<Avatar {...args} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
25
|
+
Default.args = {
|
|
26
|
+
id: 'user-1',
|
|
27
|
+
name: 'Alice Johnson',
|
|
28
|
+
image: 'https://i.pravatar.cc/150?img=1',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const WithoutImage: StoryFn<ComponentProps> = Template.bind({})
|
|
32
|
+
WithoutImage.args = {
|
|
33
|
+
id: 'user-2',
|
|
34
|
+
name: 'Bob Smith',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const SmallSize: StoryFn<ComponentProps> = Template.bind({})
|
|
38
|
+
SmallSize.args = {
|
|
39
|
+
id: 'user-3',
|
|
40
|
+
name: 'Carol Williams',
|
|
41
|
+
size: 20,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const MediumSize: StoryFn<ComponentProps> = Template.bind({})
|
|
45
|
+
MediumSize.args = {
|
|
46
|
+
id: 'user-4',
|
|
47
|
+
name: 'David Brown',
|
|
48
|
+
size: 32,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const DefaultSize: StoryFn<ComponentProps> = Template.bind({})
|
|
52
|
+
DefaultSize.args = {
|
|
53
|
+
id: 'user-5',
|
|
54
|
+
name: 'Emma Davis',
|
|
55
|
+
size: 40,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const LargeSize: StoryFn<ComponentProps> = Template.bind({})
|
|
59
|
+
LargeSize.args = {
|
|
60
|
+
id: 'user-6',
|
|
61
|
+
name: 'Frank Miller',
|
|
62
|
+
size: 56,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ExtraLargeSize: StoryFn<ComponentProps> = Template.bind({})
|
|
66
|
+
ExtraLargeSize.args = {
|
|
67
|
+
id: 'user-7',
|
|
68
|
+
name: 'Grace Lee',
|
|
69
|
+
size: 80,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const LongName: StoryFn<ComponentProps> = Template.bind({})
|
|
73
|
+
LongName.args = {
|
|
74
|
+
id: 'user-8',
|
|
75
|
+
name: 'Alexander Christopher Wellington-Montgomery III',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const DifferentColors: StoryFn = () => {
|
|
79
|
+
const users = [
|
|
80
|
+
{ id: 'user-1', name: 'Alice Anderson' },
|
|
81
|
+
{ id: 'user-2', name: 'Bob Brown' },
|
|
82
|
+
{ id: 'user-3', name: 'Carol Chen' },
|
|
83
|
+
{ id: 'user-4', name: 'David Davis' },
|
|
84
|
+
{ id: 'user-5', name: 'Emma Evans' },
|
|
85
|
+
{ id: 'user-6', name: 'Frank Foster' },
|
|
86
|
+
{ id: 'user-7', name: 'Grace Green' },
|
|
87
|
+
{ id: 'user-8', name: 'Henry Harris' },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="p-12">
|
|
92
|
+
<div className="flex gap-4 flex-wrap">
|
|
93
|
+
{users.map((user) => (
|
|
94
|
+
<div key={user.id} className="flex flex-col items-center gap-2">
|
|
95
|
+
<Avatar id={user.id} name={user.name} />
|
|
96
|
+
<span className="text-xs text-stone">{user.name}</span>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const MixedAvatars: StoryFn = () => {
|
|
105
|
+
const users = [
|
|
106
|
+
{ id: 'user-1', name: 'Alice Anderson', image: 'https://i.pravatar.cc/150?img=1' },
|
|
107
|
+
{ id: 'user-2', name: 'Bob Brown' },
|
|
108
|
+
{ id: 'user-3', name: 'Carol Chen', image: 'https://i.pravatar.cc/150?img=3' },
|
|
109
|
+
{ id: 'user-4', name: 'David Davis' },
|
|
110
|
+
{ id: 'user-5', name: 'Emma Evans', image: 'https://i.pravatar.cc/150?img=5' },
|
|
111
|
+
{ id: 'user-6', name: 'Frank Foster' },
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="p-12">
|
|
116
|
+
<div className="flex gap-4 flex-wrap items-center">
|
|
117
|
+
{users.map((user) => (
|
|
118
|
+
<div key={user.id} className="flex flex-col items-center gap-2">
|
|
119
|
+
<Avatar id={user.id} name={user.name} image={user.image} />
|
|
120
|
+
<span className="text-xs text-stone">{user.name}</span>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const VariousSizes: StoryFn = () => {
|
|
129
|
+
const sizes = [20, 32, 40, 56, 80]
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="p-12">
|
|
133
|
+
<div className="flex gap-6 items-end">
|
|
134
|
+
{sizes.map((size) => (
|
|
135
|
+
<div key={size} className="flex flex-col items-center gap-2">
|
|
136
|
+
<Avatar id="user-consistent" name="Sarah Johnson" size={size} />
|
|
137
|
+
<span className="text-xs text-stone">{size}px</span>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate avatar background and text colors based on a string
|
|
3
|
+
* Uses the ID to deterministically select from design system colors
|
|
4
|
+
*/
|
|
5
|
+
export function getAvatarColors(id: string): {
|
|
6
|
+
bgColor: string;
|
|
7
|
+
textColor: string;
|
|
8
|
+
} {
|
|
9
|
+
// Map of color combinations from Linktree design system
|
|
10
|
+
// Selected for good contrast at 20% opacity
|
|
11
|
+
const colorPairs = [
|
|
12
|
+
{ bgColor: "bg-primary bg-opacity-20", textColor: "text-primary" }, // #8129D9 - purple
|
|
13
|
+
{ bgColor: "bg-forest bg-opacity-20", textColor: "text-forest" }, // #254f1a - dark green
|
|
14
|
+
{ bgColor: "bg-iris bg-opacity-20", textColor: "text-iris" }, // #061492 - dark blue
|
|
15
|
+
{ bgColor: "bg-shade bg-opacity-20", textColor: "text-shade" }, // #1e2330 - dark blue-gray
|
|
16
|
+
{ bgColor: "bg-dahlia bg-opacity-20", textColor: "text-dahlia" }, // #502274 - dark purple
|
|
17
|
+
{ bgColor: "bg-orchid bg-opacity-20", textColor: "text-orchid" }, // #d717e7 - magenta
|
|
18
|
+
{ bgColor: "bg-currant bg-opacity-20", textColor: "text-currant" }, // #780016 - dark red
|
|
19
|
+
{ bgColor: "bg-apple bg-opacity-20", textColor: "text-apple" }, // #c41500 - red
|
|
20
|
+
{ bgColor: "bg-rose bg-opacity-20", textColor: "text-rose" }, // #fc3e4b - pink
|
|
21
|
+
{ bgColor: "bg-root bg-opacity-20", textColor: "text-root" }, // #4c2e05 - brown
|
|
22
|
+
{ bgColor: "bg-poppy bg-opacity-20", textColor: "text-poppy" }, // #ff6c02 - orange
|
|
23
|
+
{ bgColor: "bg-moss bg-opacity-20", textColor: "text-moss" }, // #70764d - olive green
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Simple hash function to get consistent index
|
|
27
|
+
let hash = 0;
|
|
28
|
+
for (let i = 0; i < id.length; i++) {
|
|
29
|
+
hash = ((hash << 5) - hash) + id.charCodeAt(i);
|
|
30
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const index = Math.abs(hash) % colorPairs.length;
|
|
34
|
+
return colorPairs[index];
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { getAvatarColors } from './avatarColors';
|
|
4
|
+
|
|
5
|
+
export interface AvatarProps {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
size?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Avatar component that displays a user image or colored initial fallback
|
|
15
|
+
*/
|
|
16
|
+
export const Avatar: React.FC<AvatarProps> = ({
|
|
17
|
+
id,
|
|
18
|
+
name,
|
|
19
|
+
image,
|
|
20
|
+
size = 40,
|
|
21
|
+
className,
|
|
22
|
+
}) => {
|
|
23
|
+
const { bgColor, textColor } = getAvatarColors(id);
|
|
24
|
+
const initial = name.charAt(0).toUpperCase();
|
|
25
|
+
|
|
26
|
+
// Determine font size based on avatar size
|
|
27
|
+
const getFontSizeClass = () => {
|
|
28
|
+
if (size < 32) return 'text-xs';
|
|
29
|
+
if (size < 56) return 'text-sm';
|
|
30
|
+
return 'text-lg';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const fontSizeClass = getFontSizeClass();
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={classNames(
|
|
38
|
+
'flex-shrink-0 overflow-hidden rounded-full',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
style={{ width: `${size}px`, height: `${size}px` }}
|
|
42
|
+
>
|
|
43
|
+
{image ? (
|
|
44
|
+
<img
|
|
45
|
+
src={image}
|
|
46
|
+
alt=""
|
|
47
|
+
className="h-full w-full object-cover aspect-square"
|
|
48
|
+
/>
|
|
49
|
+
) : (
|
|
50
|
+
<div
|
|
51
|
+
className={classNames(
|
|
52
|
+
'flex h-full w-full items-center justify-center font-semibold',
|
|
53
|
+
bgColor,
|
|
54
|
+
textColor,
|
|
55
|
+
fontSizeClass
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{initial}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from '@storybook/react'
|
|
2
|
+
import { ChannelList } from '.'
|
|
3
|
+
import { MockChatProvider } from '../../stories/mocks'
|
|
4
|
+
|
|
5
|
+
import React from 'react'
|
|
6
|
+
|
|
7
|
+
type ComponentProps = React.ComponentProps<typeof ChannelList>
|
|
8
|
+
|
|
9
|
+
const meta: Meta<ComponentProps> = {
|
|
10
|
+
title: 'ChannelList',
|
|
11
|
+
component: ChannelList,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'fullscreen',
|
|
14
|
+
},
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => (
|
|
17
|
+
<MockChatProvider>
|
|
18
|
+
<Story />
|
|
19
|
+
</MockChatProvider>
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
export default meta
|
|
24
|
+
|
|
25
|
+
const Template: StoryFn<ComponentProps> = (args) => {
|
|
26
|
+
return (
|
|
27
|
+
<div className="h-screen w-full md:w-[360px] bg-chalk">
|
|
28
|
+
<ChannelList {...args} />
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Default: StoryFn<ComponentProps> = Template.bind({})
|
|
34
|
+
Default.args = {
|
|
35
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
36
|
+
participantLabel: 'participants',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const WithStartConversation: StoryFn<ComponentProps> = Template.bind({})
|
|
40
|
+
WithStartConversation.args = {
|
|
41
|
+
onChannelSelect: (channel) => console.log('Channel selected:', channel.id),
|
|
42
|
+
showStartConversation: true,
|
|
43
|
+
onStartConversation: () => console.log('Start conversation clicked'),
|
|
44
|
+
participantLabel: 'followers',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|