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 type { ButtonProps } from '@heroui/react'
import { Button, cn } from '@heroui/react' import { Button, cn } from '@heroui/react'
import { PlusIcon } from 'lucide-react' import { PlusIcon } from 'lucide-react'
import type { FC } from 'react'
interface Props extends ButtonProps { const AddButton = ({ children, className, ...props }: ButtonProps) => {
children: React.ReactNode
}
const AddButton: FC<Props> = ({ children, className, ...props }) => {
return ( return (
<Button <Button
{...props}
onPress={props.onPress}
className={cn( 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)]', '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 className
)} )}
startContent={<PlusIcon size={16} className="shrink-0" />}> startContent={<PlusIcon size={16} className="shrink-0" />}
{...props}>
{children} {children}
</Button> </Button>
) )

View File

@ -1,3 +1,4 @@
import { cn } from '@heroui/react'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons' import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
@ -19,14 +20,14 @@ import {
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuTrigger ContextMenuTrigger
} from '@renderer/ui/context-menu' } from '@renderer/ui/context-menu'
import { classNames } from '@renderer/utils'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { MenuIcon, XIcon } from 'lucide-react' import { MenuIcon, XIcon } from 'lucide-react'
import type { FC } from 'react' import type { FC } from 'react'
import React, { memo, startTransition, useEffect, useMemo, useState } from 'react' import React, { memo, startTransition, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { ListItem, ListItemEditInput, ListItemName, ListItemNameContainer, MenuButton, StatusIndicator } from './shared'
// const logger = loggerService.withContext('AgentItem') // const logger = loggerService.withContext('AgentItem')
@ -67,7 +68,6 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
</div> </div>
}> }>
<MenuButton <MenuButton
className="menu"
onClick={(e: React.MouseEvent) => { onClick={(e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
if (isConfirmingDeletion || e.ctrlKey || e.metaKey) { if (isConfirmingDeletion || e.ctrlKey || e.metaKey) {
@ -115,20 +115,21 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
<> <>
<ContextMenu modal={false}> <ContextMenu modal={false}>
<ContextMenuTrigger> <ContextMenuTrigger>
<SessionListItem <ListItem
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} className={cn(
isActive ? 'active' : undefined,
singlealone ? 'singlealone' : undefined,
isEditing ? 'cursor-default' : 'cursor-pointer',
'rounded-[var(--list-item-border-radius)]'
)}
onClick={isEditing ? undefined : onPress} onClick={isEditing ? undefined : onPress}
onDoubleClick={() => startEdit(session.name ?? '')} onDoubleClick={() => startEdit(session.name ?? '')}
title={session.name ?? session.id} title={session.name ?? session.id}>
style={{ {isPending && !isActive && <StatusIndicator variant="pending" />}
borderRadius: 'var(--list-item-border-radius)', {isFulfilled && !isActive && <StatusIndicator variant="fulfilled" />}
cursor: isEditing ? 'default' : 'pointer' <ListItemNameContainer>
}}>
{isPending && !isActive && <PendingIndicator />}
{isFulfilled && !isActive && <FulfilledIndicator />}
<SessionNameContainer>
{isEditing ? ( {isEditing ? (
<SessionEditInput <ListItemEditInput
ref={inputRef} ref={inputRef}
value={editValue} value={editValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)} 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} /> <SessionLabel session={session} />
</SessionName> </ListItemName>
<DeleteButton /> <DeleteButton />
</> </>
)} )}
</SessionNameContainer> </ListItemNameContainer>
</SessionListItem> </ListItem>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <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) export default memo(SessionItem)

View File

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

View File

@ -1,3 +1,4 @@
import { cn } from '@heroui/react'
import { DraggableVirtualList } from '@renderer/components/DraggableList' import { DraggableVirtualList } from '@renderer/components/DraggableList'
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons' import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup' import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -17,7 +18,7 @@ import store from '@renderer/store'
import { newMessagesActions } from '@renderer/store/newMessage' import { newMessagesActions } from '@renderer/store/newMessage'
import { setGenerating } from '@renderer/store/runtime' import { setGenerating } from '@renderer/store/runtime'
import type { Assistant, Topic } from '@renderer/types' 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 { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
import { import {
exportMarkdownToJoplin, exportMarkdownToJoplin,
@ -53,6 +54,15 @@ import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components' import styled from 'styled-components'
import AddButton from './AddButton' import AddButton from './AddButton'
import {
ListContainer,
ListItem,
ListItemEditInput,
ListItemName,
ListItemNameContainer,
MenuButton,
StatusIndicator
} from './shared'
interface Props { interface Props {
assistant: Assistant 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 topicFulfilledQuery = useSelector((state: RootState) => state.messages.fulfilledByTopic)
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics) 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 [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
const deleteTimerRef = useRef<NodeJS.Timeout>(null) const deleteTimerRef = useRef<NodeJS.Timeout>(null)
const [editingTopicId, setEditingTopicId] = useState<string | null>(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' const singlealone = topicPosition === 'right' && position === 'right'
return ( return (
<DraggableVirtualList <ListContainer className="topics-tab">
className="topics-tab" <AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
list={sortedTopics} {t('chat.add.topic.title')}
onUpdate={updateTopics} </AddButton>
style={{ height: '100%', padding: '11px 0 10px 10px' }} <DraggableVirtualList list={sortedTopics} onUpdate={updateTopics} className="overflow-y-auto overflow-x-hidden">
itemContainerStyle={{ paddingBottom: '8px' }} {(topic) => {
header={ const isActive = topic.id === activeTopic?.id
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2"> const topicName = topic.name.replace('`', '')
{t('chat.add.topic.title')} const topicPrompt = topic.prompt
</AddButton> const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
const topicName = topic.name.replace('`', '')
const topicPrompt = topic.prompt
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
const getTopicNameClassName = () => { const getTopicNameClassName = () => {
if (isRenaming(topic.id)) return 'shimmer' if (isRenaming(topic.id)) return 'shimmer'
if (isNewlyRenamed(topic.id)) return 'typing' if (isNewlyRenamed(topic.id)) return 'typing'
return '' return ''
} }
return ( return (
<Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}> <Dropdown menu={{ items: getTopicMenuItems }} trigger={['contextMenu']}>
<TopicListItem <ListItem
onContextMenu={() => setTargetTopic(topic)} onContextMenu={() => setTargetTopic(topic)}
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')} className={cn(
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)} isActive ? 'active' : undefined,
style={{ singlealone ? 'singlealone' : undefined,
borderRadius, editingTopicId === topic.id && topicEdit.isEditing ? 'cursor-default' : 'cursor-pointer',
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer' showTopicTime ? 'rounded-2xl' : 'rounded-[var(--list-item-border-radius)]'
}}>
{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>
)} )}
{!topic.pinned && ( onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}>
<Tooltip {isPending(topic.id) && !isActive && <StatusIndicator variant="pending" />}
placement="bottom" {isFulfilled(topic.id) && !isActive && <StatusIndicator variant="fulfilled" />}
mouseEnterDelay={0.7} <ListItemNameContainer>
mouseLeaveDelay={0} {editingTopicId === topic.id && topicEdit.isEditing ? (
title={ <ListItemEditInput
<div style={{ fontSize: '12px', opacity: 0.8, fontStyle: 'italic' }}> ref={topicEdit.inputRef}
{t('chat.topics.delete.shortcut', { key: isMac ? '⌘' : 'Ctrl' })} value={topicEdit.editValue}
</div> onChange={topicEdit.handleInputChange}
}> onKeyDown={topicEdit.handleKeyDown}
<MenuButton onClick={(e) => e.stopPropagation()}
className="menu" />
onClick={(e) => { ) : (
if (e.ctrlKey || e.metaKey) { <ListItemName
handleConfirmDelete(topic, e) className={getTopicNameClassName()}
} else if (deletingTopicId === topic.id) { title={topicName}
handleConfirmDelete(topic, e) onDoubleClick={() => {
} else { setEditingTopicId(topic.id)
handleDeleteClick(topic.id, e) topicEdit.startEdit(topic.name)
}
}}> }}>
{deletingTopicId === topic.id ? ( {topicName}
<DeleteIcon size={14} color="var(--color-error)" style={{ pointerEvents: 'none' }} /> </ListItemName>
) : ( )}
<XIcon size={14} color="var(--color-text-3)" style={{ pointerEvents: 'none' }} /> {!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> </MenuButton>
</Tooltip> )}
</ListItemNameContainer>
{topicPrompt && (
<TopicPromptText className="prompt" title={fullTopicPrompt}>
{fullTopicPrompt}
</TopicPromptText>
)} )}
{topic.pinned && ( {showTopicTime && (
<MenuButton className="pin"> <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
<PinIcon size={14} color="var(--color-text-3)" />
</MenuButton>
)} )}
</TopicNameContainer> </ListItem>
{topicPrompt && ( </Dropdown>
<TopicPromptText className="prompt" title={fullTopicPrompt}> )
{fullTopicPrompt} }}
</TopicPromptText> </DraggableVirtualList>
)} </ListContainer>
{showTopicTime && <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>}
</TopicListItem>
</Dropdown>
)
}}
</DraggableVirtualList>
) )
} }
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` const TopicPromptText = styled.div`
color: var(--color-text-2); color: var(--color-text-2);
font-size: 12px; font-size: 12px;
@ -751,15 +614,3 @@ const TopicTime = styled.div`
color: var(--color-text-3); color: var(--color-text-3);
font-size: 11px; 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>
)
}