Both were built and tested but never wired in — SeancePage.tsx was reimplementing the same markup inline. Removes the duplication.
105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import { useTranslation } from 'react-i18next'
|
|
import type { SpiritEntity } from '../lib/types'
|
|
|
|
export type EntityCardProps = {
|
|
entity: SpiritEntity | null
|
|
isNew: boolean
|
|
/** Whether the collapsible dossier body is expanded. */
|
|
open: boolean
|
|
/** Toggle the collapsed/expanded state (ignored while there's no entity). */
|
|
onToggleOpen: () => void
|
|
}
|
|
|
|
/**
|
|
* Occult dossier of the currently-summoned spirit, wax-seal vibe.
|
|
* Styling (`.spirit-dossier` / `.dossier-*` / `.rarity-badge` / …) lives in
|
|
* SeancePage.css alongside the rest of the side column.
|
|
*/
|
|
export function EntityCard({ entity, isNew, open, onToggleOpen }: EntityCardProps) {
|
|
const { t } = useTranslation()
|
|
|
|
if (!entity) {
|
|
return (
|
|
<section className="spirit-dossier empty">
|
|
<div className="dossier-body">
|
|
<p className="dossier-none">{t('seance.entity.none')}</p>
|
|
<p className="dossier-nonehint dim">{t('seance.entity.noneHint')}</p>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<section className={`spirit-dossier ${open ? 'open' : 'closed'}`}>
|
|
<button type="button" className="dossier-header" onClick={onToggleOpen} aria-expanded={open}>
|
|
<span className="dossier-title">{t('seance.entity.title')}</span>
|
|
<span className="dossier-chevron" aria-hidden>
|
|
{open ? '▾' : '◂'}
|
|
</span>
|
|
</button>
|
|
{open && (
|
|
<div className="dossier-body">
|
|
<h3 className="dossier-name" style={{ color: `hsl(${entity.visual.hue} 85% 78%)` }}>
|
|
{entity.name}
|
|
</h3>
|
|
<p className="dossier-epithet">{entity.epithet}</p>
|
|
<div className="dossier-badges">
|
|
<span className={`rarity-badge rarity-${entity.rarity}`}>
|
|
{t(`seance.entity.rarity.${entity.rarity}`)}
|
|
</span>
|
|
{isNew ? (
|
|
<span className="dossier-badge badge-new">{t('seance.entity.isNew')}</span>
|
|
) : (
|
|
<span className="dossier-badge badge-known">
|
|
{t('seance.entity.known', { count: entity.contact_count })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="dossier-section">
|
|
<h4>{t('seance.entity.persona')}</h4>
|
|
<p className="dossier-persona">{entity.persona}</p>
|
|
</div>
|
|
|
|
<div className="dossier-section">
|
|
<h4>{t('seance.entity.voice')}</h4>
|
|
<dl className="voice-params">
|
|
<div>
|
|
<dt>id</dt>
|
|
<dd>{entity.voice.voice_id}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>pitch</dt>
|
|
<dd>{entity.voice.pitch.toFixed(2)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>rate</dt>
|
|
<dd>{entity.voice.rate.toFixed(2)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>noise</dt>
|
|
<dd>{entity.voice.noise.toFixed(2)}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>echo</dt>
|
|
<dd>{entity.voice.echo.toFixed(2)}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
{entity.quotes.length > 0 && (
|
|
<div className="dossier-section">
|
|
<h4>{t('seance.entity.quotes')}</h4>
|
|
<ul className="dossier-quotes">
|
|
{entity.quotes.slice(0, 4).map((q, i) => (
|
|
<li key={i}>“{q}”</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)
|
|
}
|