video-flow-b/components/ui/CharacterToken.tsx
2025-08-29 03:18:20 +08:00

83 lines
2.4 KiB
TypeScript

import { Node, mergeAttributes } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, ReactNodeViewProps } from '@tiptap/react'
import { motion, AnimatePresence } from 'framer-motion'
import { useState } from 'react'
export const CharacterToken = Node.create({
name: 'characterToken',
group: 'inline',
inline: true,
atom: true,
selectable: true,
addAttributes() {
return {
id: { default: null },
name: { default: 'Role name' },
avatar: { default: '' },
gender: { default: 'Unknown' },
age: { default: '-' },
}
},
parseHTML() {
return [{ tag: 'span[data-character]' }]
},
renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes({ 'data-character': '' }, HTMLAttributes), HTMLAttributes.name]
},
addNodeView() {
return ReactNodeViewRenderer(CharacterView)
},
})
function CharacterView(props: ReactNodeViewProps) {
const { node } = props;
const [showCard, setShowCard] = useState(false)
const { name, avatar, gender, age } = node.attrs
const handleClick = () => {
console.log('Click role:', name)
alert(`Click role: ${name}`)
}
return (
<NodeViewWrapper
as="span"
contentEditable={false}
className="relative inline-block px-2 py-0.5 rounded bg-yellow-100 text-yellow-900 font-semibold cursor-pointer border border-yellow-300 shadow-sm hover:bg-yellow-200"
onMouseEnter={() => setShowCard(true)}
onMouseLeave={() => setShowCard(false)}
onClick={handleClick}
>
{name}
<AnimatePresence>
{showCard && (
<motion.div
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2 }}
className="absolute top-full left-0 mt-2 w-60 rounded-lg bg-white text-black shadow-xl p-3 z-50"
>
<div className="flex items-center gap-3">
<img
src={avatar || 'https://placekitten.com/64/64'}
alt={name}
className="w-12 h-12 rounded-full border"
/>
<div>
<div className="font-semibold text-base">{name}</div>
<div className="text-sm text-gray-600">{gender} / {age}</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</NodeViewWrapper>
)
}