Files
bilin/src/renderer/components/reference/ReferencePanel.tsx
T

143 lines
5.0 KiB
TypeScript

import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBookStore } from '@renderer/stores/useBookStore'
import { useReferenceStore } from '@renderer/stores/useReferenceStore'
import { useEditStore } from '@renderer/stores/useEditStore'
type RefItem = {
id: string
kind: 'setting' | 'outline' | 'inspiration'
title: string
preview: string
}
export function ReferencePanel(): React.JSX.Element {
const { t } = useTranslation()
const open = useReferenceStore((s) => s.open)
const pinned = useReferenceStore((s) => s.pinned)
const tab = useReferenceStore((s) => s.tab)
const query = useReferenceStore((s) => s.query)
const setTab = useReferenceStore((s) => s.setTab)
const setQuery = useReferenceStore((s) => s.setQuery)
const setOpen = useReferenceStore((s) => s.setOpen)
const setPinned = useReferenceStore((s) => s.setPinned)
const switchTarget = useEditStore((s) => s.switchTarget)
const outlines = useBookStore((s) => s.outlines)
const settings = useBookStore((s) => s.settings)
const inspirations = useBookStore((s) => s.inspirations)
const [detailItem, setDetailItem] = useState<RefItem | null>(null)
const items = useMemo(() => {
const q = query.trim().toLowerCase()
const match = (title: string, body: string) =>
!q || title.toLowerCase().includes(q) || body.toLowerCase().includes(q)
if (tab === 'setting') {
return settings
.filter((s) => match(s.name, s.description))
.map((s) => ({ id: s.id, kind: 'setting' as const, title: s.name, preview: s.description }))
}
if (tab === 'outline') {
return outlines
.filter((o) => match(o.title, o.description))
.map((o) => ({ id: o.id, kind: 'outline' as const, title: o.title, preview: o.description }))
}
return inspirations
.filter((i) => match(i.title, i.content))
.map((i) => ({ id: i.id, kind: 'inspiration' as const, title: i.title, preview: i.content }))
}, [tab, query, settings, outlines, inspirations])
if (!open) return <div id="reference-panel" />
return (
<div id="reference-panel" className="open" data-testid="reference-panel">
<div className="ref-header">
<span>{t('reference.title')}</span>
<div className="ref-header-actions">
<button
type="button"
className={`btn ref-pin-btn ${pinned ? 'active' : ''}`}
data-testid="reference-pin"
title={pinned ? t('reference.unpin') : t('reference.pin')}
onClick={() => setPinned(!pinned)}
>
📌
</button>
<button
type="button"
className="btn"
data-testid="reference-close"
onClick={() => {
if (pinned) setPinned(false)
else setOpen(false)
}}
>
{pinned ? t('reference.unpin') : '✕'}
</button>
</div>
</div>
<input
className="form-control form-control--wide"
placeholder={t('reference.search')}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className="ref-tabs">
{(['setting', 'outline', 'inspiration'] as const).map((id) => (
<button
key={id}
type="button"
className={`ref-tab ${tab === id ? 'active' : ''}`}
onClick={() => setTab(id)}
>
{t(`reference.tab.${id}`)}
</button>
))}
</div>
<div className="ref-list">
{items.map((item) => (
<div
key={item.id}
className={`ref-item ${detailItem?.id === item.id ? 'active' : ''}`}
draggable
onDragStart={(e) => {
const plain = item.preview.replace(/<[^>]+>/g, '').slice(0, 200)
e.dataTransfer.setData('text/plain', plain)
}}
onClick={() => setDetailItem(item)}
onDoubleClick={() => void switchTarget({ kind: item.kind, id: item.id })}
role="button"
tabIndex={0}
data-testid={`ref-item-${item.id}`}
>
<div className="ref-item-title">{item.title}</div>
<div className="ref-item-preview">
{item.preview.replace(/<[^>]+>/g, '').slice(0, 40)}
</div>
</div>
))}
</div>
{detailItem && (
<div className="ref-detail-drawer" data-testid="reference-detail-drawer">
<div className="ref-detail-header">
<strong>{detailItem.title}</strong>
<button type="button" className="btn" onClick={() => setDetailItem(null)}>
</button>
</div>
<div className="ref-detail-body">
{detailItem.preview.replace(/<[^>]+>/g, '')}
</div>
<button
type="button"
className="btn btn-primary"
onClick={() => void switchTarget({ kind: detailItem.kind, id: detailItem.id })}
>
{t('reference.openInEditor')}
</button>
</div>
)}
</div>
)
}