Remove unused chat directory and related components (#2479)

Removes the entire `frontend/app/view/chat/` directory and its
dependencies—avatar and collapsiblemenu components—which were only
referenced within the chat code itself.

**Removed:**
- `frontend/app/view/chat/` directory (13 files: channels, chat,
chatbox, chatmessages, userlist + their styles/stories)
- `frontend/app/element/avatar.{tsx,scss,stories.tsx}` (only used by
chat/userlist)
- `frontend/app/element/collapsiblemenu.{tsx,scss,stories.tsx}` (only
used by chat/channels)
- `avatar-dims-mixin` from `frontend/app/mixins.scss` (only used by
avatar.scss)

Total: 19 files, ~1300 lines removed.


---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
This commit is contained in:
Copilot 2025-10-25 22:33:27 -07:00 committed by GitHub
parent f86de16983
commit e27717461e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 0 additions and 1300 deletions

View file

@ -1,57 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
@use "../mixins.scss";
.avatar {
position: relative;
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--border-color);
display: flex;
justify-content: center;
align-items: center;
color: var(--main-text-color);
font-size: 18px;
text-transform: uppercase;
.avatar-image {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar-initials {
font-weight: bold;
}
.status-indicator {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: transparent;
&.online {
background-color: var(--success-color);
}
&.offline {
background-color: var(--grey-text-color);
}
&.busy {
background-color: var(--error-color);
}
&.away {
background-color: var(--warning-color);
}
}
@include mixins.avatar-dims-mixin();
}

View file

@ -1,68 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";
import { Avatar } from "./avatar";
const meta = {
title: "Elements/Avatar",
component: Avatar,
args: {
name: "John Doe",
status: "offline",
imageUrl: "",
},
argTypes: {
name: {
control: { type: "text" },
description: "The name of the user",
},
status: {
control: { type: "select", options: ["online", "offline", "busy", "away"] },
description: "The status of the user",
},
imageUrl: {
control: { type: "text" },
description: "Optional image URL for the avatar",
},
},
} satisfies Meta<typeof Avatar>;
export default meta;
type Story = StoryObj<typeof meta>;
// Default case (without an image, default status: offline)
export const Default: Story = {
args: {
name: "John Doe",
status: "offline",
imageUrl: "",
},
};
// Online status with an image
export const OnlineWithImage: Story = {
args: {
name: "Alice Smith",
status: "online",
imageUrl: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
},
};
// Busy status without an image
export const BusyWithoutImage: Story = {
args: {
name: "Michael Johnson",
status: "busy",
imageUrl: "",
},
};
// Away status with an image
export const AwayWithImage: Story = {
args: {
name: "Sarah Connor",
status: "away",
imageUrl: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
},
};

View file

@ -1,36 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { memo } from "react";
import clsx from "clsx";
import "./avatar.scss";
interface AvatarProps {
name: string;
status: "online" | "offline" | "busy" | "away";
className?: string;
imageUrl?: string;
}
const Avatar = memo(({ name, status = "offline", className, imageUrl }: AvatarProps) => {
const getInitials = (name: string) => {
const nameParts = name.split(" ");
const initials = nameParts.map((part) => part[0]).join("");
return initials.toUpperCase();
};
return (
<div className={clsx("avatar", status, className)} title="status">
{imageUrl ? (
<img src={imageUrl} alt={`${name}'s avatar`} className="avatar-image" />
) : (
<div className="avatar-initials">{getInitials(name)}</div>
)}
<div className={`status-indicator ${status}`} />
</div>
);
});
Avatar.displayName = "Avatar";
export { Avatar };

View file

@ -1,62 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
@use "../mixins.scss";
.collapsible-menu {
list-style: none;
padding: 0;
}
.collapsible-menu-item {
cursor: pointer;
user-select: none;
padding: 0;
}
.collapsible-menu-item-button {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.collapsible-menu-item-content {
display: flex;
align-items: center;
}
.collapsible-menu-item-icon {
margin-right: 10px; /* Space between icon and text */
}
.collapsible-menu-item-text {
text-decoration: none;
}
.nested-list {
list-style: none;
padding-left: 20px;
}
.nested-list.open {
display: block;
}
.nested-list.closed {
display: none;
}
.collapsible-menu-item-button {
padding: 10px;
color: var(--main-text-color);
&:hover {
background-color: var(--button-grey-hover-bg);
border-radius: 4px;
}
}
.collapsible-menu-item-button.clickable:hover {
background-color: #f0f0f0;
}

View file

@ -1,170 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Meta, StoryObj } from "@storybook/react";
import { Avatar } from "./avatar";
import { CollapsibleMenu } from "./collapsiblemenu";
const meta: Meta<typeof CollapsibleMenu> = {
title: "Elements/CollapsibleMenu",
component: CollapsibleMenu,
argTypes: {
items: { control: "object" },
renderItem: { control: false },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// Container style for limiting the width to 360px
const Container = (props: any) => (
<div
style={{ width: "360px", margin: "0 auto", border: "1px solid #ccc", padding: "10px", boxSizing: "border-box" }}
>
{props.children}
</div>
);
const basicItems = [
{
label: "Inbox",
icon: <i className="fa-sharp fa-solid fa-inbox"></i>,
onClick: () => console.log("Inbox clicked"),
},
{
label: "Sent Mail",
icon: <i className="fa-sharp fa-solid fa-paper-plane"></i>,
onClick: () => console.log("Sent Mail clicked"),
},
{
label: "Drafts",
icon: <i className="fa-sharp fa-solid fa-drafting-compass"></i>,
onClick: () => console.log("Drafts clicked"),
},
];
const nestedItems = [
{
label: "Inbox",
icon: <i className="fa-sharp fa-solid fa-inbox"></i>,
onClick: () => console.log("Inbox clicked"),
subItems: [
{
label: "Starred",
icon: <i className="fa-sharp fa-solid fa-star"></i>,
onClick: () => console.log("Starred clicked"),
},
{
label: "Important",
icon: <i className="fa-sharp fa-solid fa-star"></i>,
onClick: () => console.log("Important clicked"),
},
],
},
{
label: "Sent Mail",
icon: <i className="fa-sharp fa-solid fa-paper-plane"></i>,
onClick: () => console.log("Sent Mail clicked"),
},
{
label: "Drafts",
icon: <i className="fa-sharp fa-solid fa-drafting-compass"></i>,
onClick: () => console.log("Drafts clicked"),
},
];
const customRenderItem = (
item: MenuItem,
isOpen: boolean,
handleClick: (e: React.MouseEvent<any>, item: MenuItem, itemKey: string) => void
) => (
<div className="custom-list-item">
<span className="custom-list-item-icon" onClick={(e) => handleClick(e, item, `${item.label}`)}>
{item.icon}
</span>
<span className="custom-list-item-text" onClick={(e) => handleClick(e, item, `${item.label}`)}>
{item.label}
</span>
{item.subItems && <i className={`fa-sharp fa-solid ${isOpen ? "fa-angle-up" : "fa-angle-down"}`}></i>}
</div>
);
export const Default: Story = {
args: {
items: basicItems,
},
render: (args) => (
<Container>
<CollapsibleMenu {...args} />
</Container>
),
};
export const NestedList: Story = {
args: {
items: nestedItems,
},
render: (args) => (
<Container>
<CollapsibleMenu {...args} />
</Container>
),
};
export const CustomRender: Story = {
args: {
items: nestedItems,
renderItem: customRenderItem,
},
render: (args) => (
<Container>
<CollapsibleMenu {...args} />
</Container>
),
};
export const WithClickHandlers: Story = {
args: {
items: basicItems,
},
render: (args) => (
<Container>
<CollapsibleMenu {...args} />
</Container>
),
};
export const NestedWithClickHandlers: Story = {
args: {
items: nestedItems,
},
render: (args) => (
<Container>
<CollapsibleMenu {...args} />
</Container>
),
};
const avatarItems = [
{
label: "John Doe",
icon: <Avatar name="John Doe" status="online" className="size-lg" />,
onClick: () => console.log("John Doe clicked"),
},
{
label: "Jane Smith",
icon: <Avatar name="Jane Smith" status="busy" className="size-lg" />,
onClick: () => console.log("Jane Smith clicked"),
},
{
label: "Robert Brown",
icon: <Avatar name="Robert Brown" status="away" className="size-lg" />,
onClick: () => console.log("Robert Brown clicked"),
},
{
label: "Alice Lambert",
icon: <Avatar name="Alice Lambert" status="offline" className="size-lg" />,
onClick: () => console.log("Alice Lambert clicked"),
},
];

View file

@ -1,76 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import clsx from "clsx";
import React, { memo, useState } from "react";
import "./collapsiblemenu.scss";
interface VerticalNavProps {
items: MenuItem[];
className?: string;
renderItem?: (
item: MenuItem,
isOpen: boolean,
handleClick: (e: React.MouseEvent<any>, item: MenuItem, itemKey: string) => void
) => React.ReactNode;
}
const CollapsibleMenu = memo(({ items, className, renderItem }: VerticalNavProps) => {
const [open, setOpen] = useState<{ [key: string]: boolean }>({});
// Helper function to generate a unique key for each item based on its path in the hierarchy
const getItemKey = (item: MenuItem, path: string) => `${path}-${item.label}`;
const handleClick = (e: React.MouseEvent<any>, item: MenuItem, itemKey: string) => {
setOpen((prevState) => ({ ...prevState, [itemKey]: !prevState[itemKey] }));
if (item.onClick) {
item.onClick(e);
}
};
const renderListItem = (item: MenuItem, index: number, path: string) => {
const itemKey = getItemKey(item, path);
const isOpen = open[itemKey] === true;
const hasChildren = item.subItems && item.subItems.length > 0;
return (
<li key={itemKey} className="collapsible-menu-item">
{renderItem ? (
renderItem(item, isOpen, (e) => handleClick(e, item, itemKey))
) : (
<div className="collapsible-menu-item-button" onClick={(e) => handleClick(e, item, itemKey)}>
<div
className={clsx("collapsible-menu-item-content", {
"has-children": hasChildren,
"is-open": isOpen && hasChildren,
})}
>
{item.icon && <div className="collapsible-menu-item-icon">{item.icon}</div>}
<div className="collapsible-menu-item-text ellipsis">{item.label}</div>
</div>
{hasChildren && (
<i className={`fa-sharp fa-solid ${isOpen ? "fa-angle-up" : "fa-angle-down"}`}></i>
)}
</div>
)}
{hasChildren && (
<ul className={`nested-list ${isOpen ? "open" : "closed"}`}>
{item.subItems.map((child, childIndex) =>
renderListItem(child, childIndex, `${path}-${index}`)
)}
</ul>
)}
</li>
);
};
return (
<ul className={clsx("collapsible-menu", className)} role="navigation">
{items.map((item, index) => renderListItem(item, index, "root"))}
</ul>
);
});
CollapsibleMenu.displayName = "CollapsibleMenu";
export { CollapsibleMenu };

View file

@ -7,60 +7,3 @@
overflow: hidden;
text-overflow: ellipsis;
}
@mixin avatar-dims-mixin() {
&.size-xs {
width: 20px;
height: 20px;
font-size: 7px; // 18px * (20 / 50)
.status-indicator {
width: 5px; // scaled indicator size
height: 5px;
}
}
&.size-sm {
width: 30px;
height: 30px;
font-size: 11px; // 18px * (30 / 50)
.status-indicator {
width: 7px;
height: 7px;
}
}
&.size-md {
width: 40px;
height: 40px;
font-size: 14px; // 18px * (40 / 50)
.status-indicator {
width: 9px;
height: 9px;
}
}
&.size-lg {
width: 45px;
height: 45px;
font-size: 16px; // 18px * (45 / 50)
.status-indicator {
width: 10px;
height: 10px;
}
}
&.size-xl {
width: 50px;
height: 50px;
font-size: 18px; // base size
.status-indicator {
width: 12px;
height: 12px;
}
}
}

View file

@ -1,30 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.channel-list {
width: 180px;
background-color: rgba(255, 255, 255, 0.025);
overflow-x: hidden;
padding: 5px;
.menu-item-button {
padding: 5px 7px;
}
.nested-list {
padding-left: 10px;
}
.menu-item-text {
color: rgb(from var(--main-text-color) r g b / 0.7);
}
.has-children .menu-item-text {
font-size: 14px;
}
.is-open .menu-item-text {
color: var(--main-text-color);
font-weight: bold;
}
}

View file

@ -1,13 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { CollapsibleMenu } from "@/app/element/collapsiblemenu";
import { memo } from "react";
import "./channels.scss";
const Channels = memo(({ channels }: { channels: MenuItem[] }) => {
return <CollapsibleMenu className="channel-list" items={channels}></CollapsibleMenu>;
});
export { Channels };

View file

@ -1,31 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.chat-view {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
.chat-section {
display: flex;
flex-direction: column;
flex-grow: 1;
.message-wrapper {
flex-grow: 1; // Make the ChatMessages take up available height
display: flex;
flex-direction: column;
overflow: hidden; // Ensure content doesn't overflow
}
.chatbox {
display: flex;
align-items: center;
textarea {
border: none;
}
}
}
}

View file

@ -1,66 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { ChatMessage, ChatMessages } from "@/app/view/chat/chatmessages";
import { UserStatus } from "@/app/view/chat/userlist";
import * as jotai from "jotai";
import { memo } from "react";
import { Channels } from "./channels";
import { ChatBox } from "./chatbox";
import { channels, messages, users } from "./data";
import { UserList } from "./userlist";
import "./chat.scss";
class ChatModel {
viewType: string;
channels: MenuItem[];
users: UserStatus[];
messagesAtom: jotai.PrimitiveAtom<ChatMessage[]>;
constructor(blockId: string) {
this.viewType = "chat";
this.channels = channels;
this.users = users;
this.messagesAtom = jotai.atom(messages);
}
addMessageAtom = jotai.atom(null, (get, set, newMessage: ChatMessage) => {
const currentMessages = get(this.messagesAtom);
set(this.messagesAtom, [...currentMessages, newMessage]);
});
}
interface ChatProps {
model: ChatModel;
}
const Chat = memo(({ model }: ChatProps) => {
const { channels, users } = model;
const messages = jotai.useAtomValue(model.messagesAtom);
const [, appendMessage] = jotai.useAtom(model.addMessageAtom);
const handleSendMessage = (message: string) => {
const newMessage: ChatMessage = {
id: `${Date.now()}`,
username: "currentUser",
message: message,
};
appendMessage(newMessage);
};
return (
<div className="chat-view">
<Channels channels={channels}></Channels>
<div className="chat-section">
<div className="message-wrapper">
<ChatMessages messages={messages}></ChatMessages>
</div>
<ChatBox onSendMessage={(message: string) => handleSendMessage(message)} />
</div>
<UserList users={users}></UserList>
</div>
);
});
export { Chat, ChatModel };

View file

@ -1,78 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { EmojiPalette, type EmojiItem } from "@/app/element/emojipalette";
import { InputGroup } from "@/app/element/input";
import { MultiLineInput } from "@/app/element/multilineinput";
import * as keyutil from "@/util/keyutil";
import React, { memo, useRef, useState } from "react";
import { throttle } from "throttle-debounce";
interface ChatBoxProps {
onSendMessage: (message: string) => void;
}
const ChatBox = memo(({ onSendMessage }: ChatBoxProps) => {
const [message, setMessage] = useState("");
const multiLineInputRef = useRef<HTMLTextAreaElement>(null);
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
const handleKeyDown = (waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Enter") && !waveEvent.shift && message.trim() !== "") {
onSendMessage(message);
setMessage("");
return true;
}
return false;
};
const handleEmojiSelect = (emojiItem: EmojiItem) => {
if (multiLineInputRef.current) {
const { selectionStart, selectionEnd } = multiLineInputRef.current;
const currentValue = multiLineInputRef.current.value;
// Insert emoji at the current cursor position
const newValue =
currentValue.substring(0, selectionStart) + emojiItem.emoji + currentValue.substring(selectionEnd);
// Update the message state and textarea value
setMessage(newValue);
// Set the textarea value manually
multiLineInputRef.current.value = newValue;
// Move cursor after the inserted emoji
const cursorPosition = selectionStart + emojiItem.emoji.length;
// Use setTimeout to ensure the cursor positioning happens after rendering the new value
throttle(0, () => {
if (multiLineInputRef.current) {
multiLineInputRef.current.selectionStart = multiLineInputRef.current.selectionEnd = cursorPosition;
multiLineInputRef.current.focus(); // Make sure the textarea remains focused
}
})();
// Trigger onChange manually
multiLineInputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
}
};
return (
<InputGroup className="chatbox">
<MultiLineInput
ref={multiLineInputRef}
className="input"
value={message}
onChange={handleInputChange}
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
placeholder="Type a message..."
/>
<EmojiPalette placement="top-end" onSelect={handleEmojiSelect} />
</InputGroup>
);
});
export { ChatBox };

View file

@ -1,50 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.chat-messages {
height: 100%;
width: 100%;
padding: 10px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-message {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 1em;
line-height: 1.4;
}
.chat-user-icon {
height: 1em; /* Make user icon height match the text height */
width: 1em; /* Keep the icon proportional */
border-radius: 50%;
margin-right: 8px;
}
.chat-username {
font-weight: bold;
margin-right: 4px;
line-height: 1.4; /* Ensure alignment with the first line of the message */
}
.chat-text {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
}
.chat-text img {
height: 1em; /* Make inline images (rendered via markdown) match the text height */
width: auto; /* Keep the aspect ratio of images */
margin: 0 4px;
display: inline;
}
.chat-emoji {
margin: 0 2px;
font-size: 1em; /* Match emoji size with the text height */
}

View file

@ -1,90 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";
import { ChatMessages } from "./chatmessages";
import "./chatmessages.scss";
export interface ChatMessage {
id: string;
username: string;
message: string;
color?: string;
userIcon?: string;
messageIcon?: string;
}
const meta = {
title: "Elements/ChatMessages",
component: ChatMessages,
args: {
messages: [
{
id: "1",
username: "User1",
message: "Hello everyone! 👋",
color: "#ff4500",
userIcon: "https://via.placeholder.com/50",
},
{
id: "2",
username: "User2",
message: "Check this out: ![cool icon](https://via.placeholder.com/20)",
color: "#1e90ff",
},
{
id: "3",
username: "User3",
message: "This is a simple text message without icons.",
color: "#32cd32",
userIcon: "https://via.placeholder.com/50",
},
{
id: "4",
username: "User4",
message: "🎉 👏 Great job!",
color: "#ff6347",
},
{
id: "5",
username: "User5",
message: "Look at this cool icon: Isn't it awesome? ![cool icon](https://via.placeholder.com/20)",
color: "#8a2be2",
userIcon: "https://via.placeholder.com/50",
},
],
},
argTypes: {
messages: {
description: "Array of chat messages to be displayed",
},
},
} satisfies Meta<typeof ChatMessages>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Messages: Story = {
render: (args) => (
<div>
<ChatMessages {...args} />
</div>
),
};
export const ScrollableMessages: Story = {
render: (args) => (
<div style={{ height: "100%", overflow: "hidden" }}>
<ChatMessages {...args} />
</div>
),
args: {
messages: Array.from({ length: 50 }, (_, i) => ({
id: `${i + 1}`,
username: `User${i + 1}`,
message: `This is message number ${i + 1}.`,
color: i % 2 === 0 ? "#ff6347" : "#1e90ff",
userIcon: "https://via.placeholder.com/50",
})),
},
};

View file

@ -1,60 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/app/element/markdown";
import clsx from "clsx";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { memo, useEffect, useRef } from "react";
import "./chatmessages.scss";
export interface ChatMessage {
id: string;
username: string;
message: string;
color?: string;
userIcon?: string;
}
interface ChatMessagesProps {
messages: ChatMessage[];
className?: string;
}
const ChatMessages = memo(({ messages, className }: ChatMessagesProps) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
const overlayScrollRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
// scrollToBottom();
}, [messages]);
return (
<OverlayScrollbarsComponent
ref={overlayScrollRef}
className={clsx("chat-messages", className)}
options={{ scrollbars: { autoHide: "leave" } }}
>
{messages.map(({ id, username, message, color, userIcon }) => (
<div key={id} className="chat-message">
{userIcon && <img src={userIcon} alt="user icon" className="chat-user-icon" />}
<span className="chat-username" style={{ color: color || "var(--main-text-color)" }}>
{username}:
</span>
<span className="chat-text">
<Markdown scrollable={false} text={message}></Markdown>
</span>
</div>
))}
<div ref={messagesEndRef} />
</OverlayScrollbarsComponent>
);
});
ChatMessages.displayName = "ChatMessages";
export { ChatMessages };

View file

@ -1,225 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { ChatMessage } from "@/app/view/chat/chatmessages";
import { UserStatus } from "@/app/view/chat/userlist";
export const channels: MenuItem[] = [
{
label: "Aurora Streams",
icon: "#",
onClick: () => console.log("Aurora Streams clicked"),
},
{
label: "Crimson Oasis",
onClick: () => console.log("Crimson Oasis clicked"),
subItems: [
{
label: "Golden Dunes",
icon: "#",
onClick: () => console.log("Golden Dunes clicked"),
},
{
label: "Emerald Springs",
icon: "#",
onClick: () => console.log("Emerald Springs clicked"),
},
{
label: "Ruby Cascades",
icon: "#",
onClick: () => console.log("Ruby Cascades clicked"),
},
{
label: "Sapphire Falls",
icon: "#",
onClick: () => console.log("Sapphire Falls clicked"),
},
],
},
{
label: "Velvet Horizon",
onClick: () => console.log("Velvet Horizon clicked"),
subItems: [
{
label: "Amber Skies",
icon: "#",
onClick: () => console.log("Amber Skies clicked"),
},
],
},
{
label: "Mystic Meadows",
icon: "#",
onClick: () => console.log("Mystic Meadows clicked"),
},
{
label: "Celestial Grove",
icon: "#",
onClick: () => console.log("Celestial Grove clicked"),
},
{
label: "Twilight Whisper",
icon: "#",
onClick: () => console.log("Twilight Whisper clicked"),
},
{
label: "Starlit Haven",
onClick: () => console.log("Starlit Haven clicked"),
subItems: [
{
label: "Moonlit Trail",
icon: "#",
onClick: () => console.log("Moonlit Trail clicked"),
},
],
},
{
label: "Silver Mist",
icon: "#",
onClick: () => console.log("Silver Mist clicked"),
},
{
label: "Eclipse Haven",
onClick: () => console.log("Eclipse Haven clicked"),
subItems: [
{
label: "Obsidian Wave",
icon: "#",
onClick: () => console.log("Obsidian Wave clicked"),
},
{
label: "Ivory Shore",
icon: "#",
onClick: () => console.log("Ivory Shore clicked"),
},
{
label: "Azure Tide",
icon: "#",
onClick: () => console.log("Azure Tide clicked"),
},
],
},
{
label: "Dragon's Peak",
icon: "#",
onClick: () => console.log("Dragon's Peak clicked"),
},
{
label: "Seraph's Wing",
icon: "#",
onClick: () => console.log("Seraph's Wing clicked"),
},
{
label: "Frozen Abyss",
icon: "#",
onClick: () => console.log("Frozen Abyss clicked"),
},
{
label: "Radiant Blossom",
icon: "#",
onClick: () => console.log("Radiant Blossom clicked"),
},
{
label: "Whispering Pines",
icon: "#",
onClick: () => console.log("Whispering Pines clicked"),
subItems: [
{
label: "Cedar Haven",
icon: "#",
onClick: () => console.log("Cedar Haven clicked"),
},
],
},
{
label: "Scarlet Veil",
icon: "#",
onClick: () => console.log("Scarlet Veil clicked"),
},
{
label: "Onyx Spire",
icon: "#",
onClick: () => console.log("Onyx Spire clicked"),
},
{
label: "Violet Enclave",
onClick: () => console.log("Violet Enclave clicked"),
subItems: [
{
label: "Indigo Haven",
icon: "#",
onClick: () => console.log("Indigo Haven clicked"),
},
{
label: "Amethyst Hollow",
icon: "#",
onClick: () => console.log("Amethyst Hollow clicked"),
},
{
label: "Crimson Glow",
icon: "#",
onClick: () => console.log("Crimson Glow clicked"),
},
],
},
];
export const users: UserStatus[] = [
{
label: "John Doe",
status: "online",
avatarUrl: "https://via.placeholder.com/50",
onClick: () => console.log("John Doe clicked"),
},
{
label: "Jane Smith",
status: "busy",
onClick: () => console.log("Jane Smith clicked"),
},
{
label: "Robert Brown",
status: "away",
avatarUrl: "https://via.placeholder.com/50",
onClick: () => console.log("Robert Brown clicked"),
},
{
label: "Alice Lambert",
status: "offline",
onClick: () => console.log("Alice Lambert clicked"),
},
];
export const messages: ChatMessage[] = [
{
id: "1",
username: "User1",
message: "Hello everyone! 👋",
color: "#ff4500",
userIcon: "https://via.placeholder.com/50",
},
{
id: "2",
username: "User2",
message: "Check this out: ![cool icon](https://via.placeholder.com/20)",
color: "#1e90ff",
},
{
id: "3",
username: "User3",
message: "This is a simple text message without icons.",
color: "#32cd32",
userIcon: "https://via.placeholder.com/50",
},
{
id: "4",
username: "User4",
message: "🎉 👏 Great job!",
color: "#ff6347",
},
{
id: "5",
username: "User5",
message: "Look at this cool icon: Isn't it awesome? ![cool icon](https://via.placeholder.com/20)",
color: "#8a2be2",
userIcon: "https://via.placeholder.com/50",
},
];

View file

@ -1,35 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.user-list {
display: flex;
flex-direction: column;
max-width: 250px;
background-color: rgba(255, 255, 255, 0.025);
padding: 5px;
}
.user-status-item {
display: flex;
align-items: center;
padding: 8px;
margin-bottom: 6px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.user-status-item:hover {
background-color: var(--button-grey-hover-bg);
border-radius: 4px;
}
.user-status-icon {
margin-right: 10px;
display: flex;
align-items: center;
}
.user-status-text {
font-size: 1em;
font-weight: bold;
}

View file

@ -1,58 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";
import { UserList } from "./userlist";
import "./userlist.scss";
export interface UserStatus {
text: string;
status: "online" | "busy" | "away" | "offline";
onClick: () => void;
}
const meta = {
title: "Elements/UserList",
component: UserList,
args: {
users: [
{
label: "John Doe",
status: "online",
onClick: () => console.log("John Doe clicked"),
},
{
label: "Jane Smith",
status: "busy",
onClick: () => console.log("Jane Smith clicked"),
},
{
label: "Robert Brown",
status: "away",
onClick: () => console.log("Robert Brown clicked"),
},
{
label: "Alice Lambert",
status: "offline",
onClick: () => console.log("Alice Lambert clicked"),
},
],
},
argTypes: {
users: {
description: "Array of user statuses to be displayed",
},
},
} satisfies Meta<typeof UserList>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: (args) => (
<div>
<UserList {...args} />
</div>
),
};

View file

@ -1,38 +0,0 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import clsx from "clsx";
import { memo } from "react";
import { Avatar } from "../../element/avatar";
import "./userlist.scss";
export interface UserStatus {
label: string;
status: "online" | "busy" | "away" | "offline";
onClick: () => void;
avatarUrl?: string;
}
interface UserListProps {
users: UserStatus[];
className?: string;
}
const UserList = memo(({ users, className }: UserListProps) => {
return (
<div className={clsx("user-list", className)}>
{users.map(({ label, status, onClick, avatarUrl }, index) => (
<div key={index} className={clsx("user-status-item", status)} onClick={onClick}>
<div className="user-status-icon">
<Avatar name={label} status={status} className="size-sm" imageUrl={avatarUrl} />
</div>
<div className="user-status-text">{label}</div>
</div>
))}
</div>
);
});
UserList.displayName = "UserList";
export { UserList };