refactor(Topics & Sessions): Style and code structure adjustments (#10868)

* refactor(Tabs): extract shared styled components into separate file

Move common styled components (ListItem, ListItemNameContainer, ListItemName, ListItemEditInput) from SessionItem.tsx and Topics.tsx into shared.tsx to improve code reuse and maintainability

* refactor(components): extract ListContainer component for shared tab layouts

Create reusable ListContainer component to standardize layout styling across tabs
Replace manual div containers in Sessions and Topics components with new ListContainer

* refactor(ListItem): convert styled component to Tailwind CSS function component

- Convert ListItem from styled-components to Tailwind CSS function component
- Maintain all original styling and hover/active states
- Use HTMLDivElement props interface for proper TypeScript typing
- Preserve CSS custom properties for theme variables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ListItemNameContainer): convert styled component to Tailwind CSS function component

- Convert ListItemNameContainer from styled-components to Tailwind CSS function component
- Simplify layout styles using Tailwind's utility classes
- Use HTMLDivElement props interface for proper TypeScript typing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ListItemName): convert styled component to Tailwind CSS function component

- Convert ListItemName from styled-components to Tailwind CSS function component
- Use inline styles for webkit-specific line clamping properties
- Remove complex animations from component definition (can be added via CSS classes)
- Use HTMLDivElement props interface for proper TypeScript typing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ListItemEditInput): convert styled component to Tailwind CSS function component

- Convert ListItemEditInput from styled-components to Tailwind CSS function component
- Use proper InputHTMLAttributes type for input elements
- Remove styled-components import as no longer needed
- Maintain all original styling using Tailwind utility classes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(components): improve type safety and class ordering in shared components

- Replace HTMLAttributes with more specific ComponentProps types
- Reorder class names for better readability and consistency

* refactor(components): update styling and class handling in list items

- Replace deprecated classNames utility with cn from @heroui/react
- Consolidate style properties into className using cn
- Improve CSS selector syntax for better specificity
- Standardize padding and border radius values

* Revert "refactor(ListItemName): convert styled component to Tailwind CSS function component"

This reverts commit 196136068d.

* style(shared): increase font size and remove redundant padding

The font size was increased from 13px to 14px for better readability. Redundant padding in ListItemEditInput was removed to maintain consistent styling.

* refactor(AddButton): simplify component by removing FC type and inline props

Remove unnecessary FC type declaration and inline the Props interface with ButtonProps. Also clean up prop spreading by moving it to the end of the component.

* style(Topics): remove redundant className and add overflow styles

* refactor(components): extract MenuButton to shared components

Move MenuButton implementation from individual components to shared module to reduce code duplication and improve maintainability

* refactor(PendingIndicator): convert styled component to Tailwind CSS function component

- Convert PendingIndicator from styled-components to Tailwind CSS function component
- Use ComponentPropsWithoutRef<'div'> for consistent TypeScript typing
- Replace styled-components attrs with Tailwind animate-pulse class
- Use CSS custom properties for pulse-size variable
- Remove styled-components import as no longer needed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(components): replace styled indicators with shared StatusIndicator

Consolidate PendingIndicator and FulfilledIndicator into a single StatusIndicator component with variant support

* style(shared.tsx): adjust border styles for singlealone active state

* refactor: use type-only imports for react props

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Phantom 2025-11-05 14:14:40 +08:00 committed by GitHub
parent 2546dfbe5d
commit caa59c4c50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 258 additions and 397 deletions

View File

@ -1,22 +1,16 @@
import type { ButtonProps } from '@heroui/react'
import { Button, cn } from '@heroui/react'
import { PlusIcon } from 'lucide-react'
import type { FC } from 'react'
interface Props extends ButtonProps {
children: React.ReactNode
}
const AddButton: FC<Props> = ({ children, className, ...props }) => {
const AddButton = ({ children, className, ...props }: ButtonProps) => {
return (
<Button
{...props}
onPress={props.onPress}
className={cn(
'h-9 w-[calc(var(--assistants-width)-20px)] justify-start rounded-lg bg-transparent px-3 text-[13px] text-[var(--color-text-2)] hover:bg-[var(--color-list-item)]',
className
)}
startContent={<PlusIcon size={16} className="shrink-0" />}>
startContent={<PlusIcon size={16} className="shrink-0" />}
{...props}>
{children}
</Button>
)

View File

@ -1,3 +1,4 @@
import { cn } from '@heroui/react'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { isMac } from '@renderer/config/constant'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
@ -19,14 +20,14 @@ import {
ContextMenuSubTrigger,
ContextMenuTrigger
} from '@renderer/ui/context-menu'
import { classNames } from '@renderer/utils'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { Tooltip } from 'antd'
import { MenuIcon, XIcon } from 'lucide-react'
import type { FC } from 'react'
import React, { memo, startTransition, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { ListItem, ListItemEditInput, ListItemName, ListItemNameContainer, MenuButton, StatusIndicator } from './shared'
// const logger = loggerService.withContext('AgentItem')
@ -67,7 +68,6 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
</div>
}>
<MenuButton
className="menu"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
if (isConfirmingDeletion || e.ctrlKey || e.metaKey) {
@ -115,20 +115,21 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
<>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<SessionListItem
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
<ListItem
className={cn(
isActive ? 'active' : undefined,
singlealone ? 'singlealone' : undefined,
isEditing ? 'cursor-default' : 'cursor-pointer',
'rounded-[var(--list-item-border-radius)]'
)}
onClick={isEditing ? undefined : onPress}
onDoubleClick={() => startEdit(session.name ?? '')}
title={session.name ?? session.id}
style={{
borderRadius: 'var(--list-item-border-radius)',
cursor: isEditing ? 'default' : 'pointer'
}}>
{isPending && !isActive && <PendingIndicator />}
{isFulfilled && !isActive && <FulfilledIndicator />}
<SessionNameContainer>
title={session.name ?? session.id}>
{isPending && !isActive && <StatusIndicator variant="pending" />}
{isFulfilled && !isActive && <StatusIndicator variant="fulfilled" />}
<ListItemNameContainer>
{isEditing ? (
<SessionEditInput
<ListItemEditInput
ref={inputRef}
value={editValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
@ -138,14 +139,14 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
/>
) : (
<>
<SessionName>
<ListItemName>
<SessionLabel session={session} />
</SessionName>
</ListItemName>
<DeleteButton />
</>
)}
</SessionNameContainer>
</SessionListItem>
</ListItemNameContainer>
</ListItem>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
@ -188,121 +189,4 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
)
}
const SessionListItem = styled.div`
padding: 7px 12px;
border-radius: var(--list-item-border-radius);
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
width: calc(var(--assistants-width) - 20px);
margin-bottom: 8px;
.menu {
opacity: 0;
color: var(--color-text-3);
}
&:hover {
background-color: var(--color-list-item-hover);
transition: background-color 0.1s;
.menu {
opacity: 1;
}
}
&.active {
background-color: var(--color-list-item);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
.menu {
opacity: 1;
&:hover {
color: var(--color-text-2);
}
}
}
&.singlealone {
border-radius: 0 !important;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
border-left: 2px solid var(--color-primary);
box-shadow: none;
}
}
`
const SessionNameContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
height: 20px;
justify-content: space-between;
`
const SessionName = styled.div`
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 13px;
position: relative;
`
const SessionEditInput = styled.input`
background: var(--color-background);
border: none;
color: var(--color-text-1);
font-size: 13px;
font-family: inherit;
padding: 2px 6px;
width: 100%;
outline: none;
padding: 0;
`
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 20px;
min-height: 20px;
.anticon {
font-size: 12px;
}
`
const PendingIndicator = styled.div.attrs({
className: 'animation-pulse'
})`
--pulse-size: 5px;
width: 5px;
height: 5px;
position: absolute;
left: 3px;
top: 15px;
border-radius: 50%;
background-color: var(--color-status-warning);
`
const FulfilledIndicator = styled.div.attrs({
className: 'animation-pulse'
})`
--pulse-size: 5px;
width: 5px;
height: 5px;
position: absolute;
left: 3px;
top: 15px;
border-radius: 50%;
background-color: var(--color-status-success);
`
export default memo(SessionItem)

View File

@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
import AddButton from './AddButton'
import SessionItem from './SessionItem'
import { ListContainer } from './shared'
// const logger = loggerService.withContext('SessionsTab')
@ -95,7 +96,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
return (
<div className="sessions-tab flex h-full w-full flex-col p-2">
<ListContainer className="sessions-tab">
<AddButton onPress={createDefaultSession} className="mb-2" isDisabled={creatingSession}>
{t('agent.session.add.title')}
</AddButton>
@ -118,7 +119,7 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
/>
)}
</DynamicVirtualList>
</div>
</ListContainer>
)
}

View File

@ -1,3 +1,4 @@
import { cn } from '@heroui/react'
import { DraggableVirtualList } from '@renderer/components/DraggableList'
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -17,7 +18,7 @@ import store from '@renderer/store'
import { newMessagesActions } from '@renderer/store/newMessage'
import { setGenerating } from '@renderer/store/runtime'
import type { Assistant, Topic } from '@renderer/types'
import { classNames, removeSpecialCharactersForFileName } from '@renderer/utils'
import { removeSpecialCharactersForFileName } from '@renderer/utils'
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
import {
exportMarkdownToJoplin,
@ -53,6 +54,15 @@ import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import AddButton from './AddButton'
import {
ListContainer,
ListItem,
ListItemEditInput,
ListItemName,
ListItemNameContainer,
MenuButton,
StatusIndicator
} from './shared'
interface Props {
assistant: Assistant
@ -73,8 +83,6 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
const topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic)
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
@ -489,252 +497,107 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
const singlealone = topicPosition === 'right' && position === 'right'
return (
<DraggableVirtualList
className="topics-tab"
list={sortedTopics}
onUpdate={updateTopics}
style={{ height: '100%', padding: '11px 0 10px 10px' }}
itemContainerStyle={{ paddingBottom: '8px' }}
header={
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
{t('chat.add.topic.title')}
</AddButton>
}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
const topicName = topic.name.replace('`', '')
const topicPrompt = topic.prompt
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
<ListContainer className="topics-tab">
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
{t('chat.add.topic.title')}
</AddButton>
<DraggableVirtualList list={sortedTopics} onUpdate={updateTopics} className="overflow-y-auto overflow-x-hidden">
{(topic) => {
const isActive = topic.id === activeTopic?.id
const topicName = topic.name.replace('`', '')
const topicPrompt = topic.prompt
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
const getTopicNameClassName = () => {
if (isRenaming(topic.id)) return 'shimmer'
if (isNewlyRenamed(topic.id)) return 'typing'
return ''
}
const getTopicNameClassName = () => {
if (isRenaming(topic.id)) return 'shimmer'
if (isNewlyRenamed(topic.id)) return 'typing'
return ''
}
return (
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
<TopicListItem
onContextMenu={() => setTargetTopic(topic)}
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}
style={{
borderRadius,
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer'
}}>
{isPending(topic.id) && !isActive && <PendingIndicator />}
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
<TopicNameContainer>
{editingTopicId === topic.id && topicEdit.isEditing ? (
<TopicEditInput
ref={topicEdit.inputRef}
value={topicEdit.editValue}
onChange={topicEdit.handleInputChange}
onKeyDown={topicEdit.handleKeyDown}
onClick={(e) => e.stopPropagation()}
/>
) : (
<TopicName
className={getTopicNameClassName()}
title={topicName}
onDoubleClick={() => {
setEditingTopicId(topic.id)
topicEdit.startEdit(topic.name)
}}>
{topicName}
</TopicName>
return (
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
<ListItem
onContextMenu={() => setTargetTopic(topic)}
className={cn(
isActive ? 'active' : undefined,
singlealone ? 'singlealone' : undefined,
editingTopicId === topic.id && topicEdit.isEditing ? 'cursor-default' : 'cursor-pointer',
showTopicTime ? 'rounded-2xl' : 'rounded-[var(--list-item-border-radius)]'
)}
{!topic.pinned && (
<Tooltip
placement="bottom"
mouseEnterDelay={0.7}
mouseLeaveDelay={0}
title={
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
</div>
}>
<MenuButton
className="menu"
onClick={(e) => {
if (e.ctrlKey || e.metaKey) {
handleConfirmDelete(topic, e)
} else if (deletingTopicId === topic.id) {
handleConfirmDelete(topic, e)
} else {
handleDeleteClick(topic.id, e)
}
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}>
{isPending(topic.id) && !isActive && <StatusIndicator variant="pending" />}
{isFulfilled(topic.id) && !isActive && <StatusIndicator variant="fulfilled" />}
<ListItemNameContainer>
{editingTopicId === topic.id && topicEdit.isEditing ? (
<ListItemEditInput
ref={topicEdit.inputRef}
value={topicEdit.editValue}
onChange={topicEdit.handleInputChange}
onKeyDown={topicEdit.handleKeyDown}
onClick={(e) => e.stopPropagation()}
/>
) : (
<ListItemName
className={getTopicNameClassName()}
title={topicName}
onDoubleClick={() => {
setEditingTopicId(topic.id)
topicEdit.startEdit(topic.name)
}}>
{deletingTopicId === topic.id ? (
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
) : (
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
)}
{topicName}
</ListItemName>
)}
{!topic.pinned && (
<Tooltip
placement="bottom"
mouseEnterDelay={0.7}
mouseLeaveDelay={0}
title={
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}>
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })}
</div>
}>
<MenuButton
onClick={(e) => {
if (e.ctrlKey || e.metaKey) {
handleConfirmDelete(topic, e)
} else if (deletingTopicId === topic.id) {
handleConfirmDelete(topic, e)
} else {
handleDeleteClick(topic.id, e)
}
}}>
{deletingTopicId === topic.id ? (
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} />
) : (
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} />
)}
</MenuButton>
</Tooltip>
)}
{topic.pinned && (
<MenuButton className="pin">
<PinIcon size={14} color="var(--color-text-3)" />
</MenuButton>
</Tooltip>
)}
</ListItemNameContainer>
{topicPrompt && (
<TopicPromptText className="prompt" title={fullTopicPrompt}>
{fullTopicPrompt}
</TopicPromptText>
)}
{topic.pinned && (
<MenuButton className="pin">
<PinIcon size={14} color="var(--color-text-3)" />
</MenuButton>
{showTopicTime && (
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
)}
</TopicNameContainer>
{topicPrompt && (
<TopicPromptText className="prompt" title={fullTopicPrompt}>
{fullTopicPrompt}
</TopicPromptText>
)}
{showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
</TopicListItem>
</Dropdown>
)
}}
</DraggableVirtualList>
</ListItem>
</Dropdown>
)
}}
</DraggableVirtualList>
</ListContainer>
)
}
const TopicListItem = styled.div`
padding: 7px 12px;
border-radius: var(--list-item-border-radius);
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
width: calc(var(--assistants-width) - 20px);
.menu {
opacity: 0;
color: var(--color-text-3);
}
&:hover {
background-color: var(--color-list-item-hover);
transition: background-color 0.1s;
.menu {
opacity: 1;
}
}
&.active {
background-color: var(--color-list-item);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
.menu {
opacity: 1;
&:hover {
color: var(--color-text-2);
}
}
}
&.singlealone {
border-radius: 0 !important;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
border-left: 2px solid var(--color-primary);
box-shadow: none;
}
}
`
const TopicNameContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
height: 20px;
justify-content: space-between;
`
const TopicName = styled.div`
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 13px;
position: relative;
will-change: background-position, width;
--color-shimmer-mid: var(--color-text-1);
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
&.shimmer {
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
background-size: 200% 100%;
background-clip: text;
color: transparent;
animation: shimmer 3s linear infinite;
}
&.typing {
display: block;
-webkit-line-clamp: unset;
-webkit-box-orient: unset;
white-space: nowrap;
overflow: hidden;
animation: typewriter 0.5s steps(40, end);
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes typewriter {
from {
width: 0;
}
to {
width: 100%;
}
}
`
const TopicEditInput = styled.input`
background: var(--color-background);
border: none;
color: var(--color-text-1);
font-size: 13px;
font-family: inherit;
padding: 2px 6px;
width: 100%;
outline: none;
padding: 0;
`
const PendingIndicator = styled.div.attrs({
className: 'animation-pulse'
})`
--pulse-size: 5px;
width: 5px;
height: 5px;
position: absolute;
left: 3px;
top: 15px;
border-radius: 50%;
background-color: var(--color-status-warning);
`
const FulfilledIndicator = styled.div.attrs({
className: 'animation-pulse'
})`
--pulse-size: 5px;
width: 5px;
height: 5px;
position: absolute;
left: 3px;
top: 15px;
border-radius: 50%;
background-color: var(--color-status-success);
`
const TopicPromptText = styled.div`
color: var(--color-text-2);
font-size: 12px;
@ -751,15 +614,3 @@ const TopicTime = styled.div`
color: var(--color-text-3);
font-size: 11px;
`
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 20px;
min-height: 20px;
.anticon {
font-size: 12px;
}
`

View File

@ -0,0 +1,131 @@
import { cn } from '@heroui/react'
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react'
import { useMemo } from 'react'
import styled from 'styled-components'
export const ListItem = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
return (
<div
className={cn(
'mb-2 flex w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-col justify-between rounded-lg px-3 py-2 text-sm',
'transition-colors duration-100',
'hover:bg-[var(--color-list-item-hover)]',
'[.active]:bg-[var(--color-list-item)] [.active]:shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
'[&_.menu]:text-[var(--color-text-3)] [&_.menu]:opacity-0',
'hover:[&_.menu]:opacity-100',
'[.active]:[&_.menu]:opacity-100 [.active]:[&_.menu]:hover:text-[var(--color-text-2)]',
'[.singlealone.active]:border-[var(--color-primary)] [.singlealone.active]:shadow-none [.singlealone]:rounded-none [.singlealone]:border-transparent [.singlealone]:border-l-2 [.singlealone]:hover:bg-[var(--color-background-soft)]',
className
)}
{...props}>
{children}
</div>
)
}
export const ListItemNameContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
return (
<div className={cn('flex h-5 flex-row items-center justify-between gap-1', className)} {...props}>
{children}
</div>
)
}
// This component involves complex animations and will not be migrated for now.
export const ListItemName = styled.div`
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 14px;
position: relative;
will-change: background-position, width;
--color-shimmer-mid: var(--color-text-1);
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
&.shimmer {
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
background-size: 200% 100%;
background-clip: text;
color: transparent;
animation: shimmer 3s linear infinite;
}
&.typing {
display: block;
-webkit-line-clamp: unset;
-webkit-box-orient: unset;
white-space: nowrap;
overflow: hidden;
animation: typewriter 0.5s steps(40, end);
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes typewriter {
from {
width: 0;
}
to {
width: 100%;
}
}
`
export const ListItemEditInput = ({ className, ...props }: ComponentPropsWithRef<'input'>) => {
return (
<input
className={cn(
'w-full border-none bg-[var(--color-background)] p-0 font-inherit text-[var(--color-text-1)] text-sm outline-none',
className
)}
{...props}
/>
)
}
export const ListContainer = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
return (
<div className={cn('flex h-full w-full flex-col p-2', className)} {...props}>
{children}
</div>
)
}
export const MenuButton = ({ children, className, ...props }: ComponentPropsWithoutRef<'div'>) => {
return (
<div className={cn('menu', 'flex min-h-5 min-w-5 flex-row items-center justify-center', className)} {...props}>
{children}
</div>
)
}
export const StatusIndicator = ({ variant }: { variant: 'pending' | 'fulfilled' }) => {
const colors = useMemo(() => {
switch (variant) {
case 'pending':
return {
wave: 'bg-warning-400',
back: 'bg-warning-500'
}
case 'fulfilled':
return {
wave: 'bg-success-400',
back: 'bg-success-500'
}
}
}, [variant])
return (
<div className="absolute top-4 left-1 flex size-1">
<span className={cn('absolute inline-flex h-full w-full animate-ping rounded-full opacity-75', colors.wave)} />
<span className={cn('relative inline-flex size-1 rounded-full bg-warning-500', colors.back)} />
</div>
)
}