Files
ai-agent/web/components/content-editor/toolbar.tsx
T
mlogclub efe801b8bf Init
2026-04-09 10:01:23 +08:00

143 lines
4.6 KiB
TypeScript

"use client"
import "./toolbar.css"
import { useRef, type PointerEvent as ReactPointerEvent } from "react"
import { cn } from "@/lib/utils"
import type { EditorToolbarAction } from "./types"
type EditorToolbarProps = {
actions: ReadonlyArray<EditorToolbarAction>
}
function isSeparatorAction(
action: EditorToolbarAction
): action is Extract<EditorToolbarAction, { type: "separator" }> {
return "type" in action && action.type === "separator"
}
function isCustomAction(
action: EditorToolbarAction
): action is Extract<EditorToolbarAction, { type: "custom" }> {
return "type" in action && action.type === "custom"
}
export function EditorToolbar({ actions }: EditorToolbarProps) {
const containerRef = useRef<HTMLDivElement>(null)
const dragStateRef = useRef<{
pointerId: number
startX: number
startScrollLeft: number
moved: boolean
} | null>(null)
const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
if (event.pointerType === "mouse" && event.button !== 0) {
return
}
const container = containerRef.current
if (!container || container.scrollWidth <= container.clientWidth) {
return
}
dragStateRef.current = {
pointerId: event.pointerId,
startX: event.clientX,
startScrollLeft: container.scrollLeft,
moved: false,
}
container.setPointerCapture(event.pointerId)
}
const handlePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
const container = containerRef.current
const dragState = dragStateRef.current
if (!container || !dragState || dragState.pointerId !== event.pointerId) {
return
}
const deltaX = event.clientX - dragState.startX
if (Math.abs(deltaX) > 3) {
dragState.moved = true
}
container.scrollLeft = dragState.startScrollLeft - deltaX
}
const handlePointerEnd = (event: ReactPointerEvent<HTMLDivElement>) => {
const container = containerRef.current
const dragState = dragStateRef.current
if (!container || !dragState || dragState.pointerId !== event.pointerId) {
return
}
if (container.hasPointerCapture(event.pointerId)) {
container.releasePointerCapture(event.pointerId)
}
window.setTimeout(() => {
dragStateRef.current = null
}, 0)
}
return (
<div
ref={containerRef}
className="content-editor-toolbar overflow-x-auto overflow-y-hidden border-b border-border px-1 py-1 h-9.25"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
onPointerCancel={handlePointerEnd}
>
<div className="flex min-w-max items-center justify-between">
<div className="flex items-center">
{actions.map((action) => {
if (isSeparatorAction(action)) {
return (
<span
key={action.key}
aria-hidden="true"
className="relative mx-2 inline-block h-[0.9em] w-px self-center bg-border"
/>
)
}
if (isCustomAction(action)) {
return <div key={action.key}>{action.content}</div>
}
const Icon = action.icon
return (
<button
key={action.key}
type="button"
aria-label={action.label}
title={action.label}
disabled={action.disabled}
onClick={action.onClick}
onClickCapture={(event) => {
if (dragStateRef.current?.moved) {
event.preventDefault()
event.stopPropagation()
}
}}
data-pressed={action.pressed ? "true" : "false"}
className={cn(
"mx-[2px] cursor-pointer list-none rounded-[3px] border-none bg-transparent text-[#3f4a54] transition-all duration-300 select-none hover:bg-[#f2f2f2] disabled:cursor-not-allowed disabled:opacity-60 data-[pressed=true]:bg-[#f2f2f2] dark:text-[#999] dark:hover:bg-[#333] dark:data-[pressed=true]:bg-[#333]",
action.icon && !action.content
? "flex flex-col items-center px-[2px] py-0"
: "flex flex-col items-center px-[6px] py-0"
)}
>
{Icon ? <Icon className="box-content size-4 p-1" /> : null}
{action.content ? (
<span className="text-[12px] leading-none whitespace-nowrap">
{action.content}
</span>
) : null}
</button>
)
})}
</div>
</div>
</div>
)
}