refactor: use TelemetryReadout and EntityCard components

Both were built and tested but never wired in — SeancePage.tsx was
reimplementing the same markup inline. Removes the duplication.
This commit is contained in:
Indiana
2026-07-21 03:45:11 +00:00
parent 0756e677b9
commit f0ee7dbdab
3 changed files with 129 additions and 192 deletions

View File

@@ -1,79 +1,104 @@
import { useTranslation } from 'react-i18next'
import type { SpiritEntity } from '../lib/types'
import { GhostGlyph } from './GhostGlyph'
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
}
const RARITY_CLASS: Record<string, string> = {
common: 'rarity-common',
uncommon: 'rarity-uncommon',
rare: 'rarity-rare',
mythic: 'rarity-mythic',
}
/** Occult dossier of the currently-summoned spirit, wax-seal vibe. */
export function EntityCard({ entity, isNew }: EntityCardProps) {
/**
* 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 (
<aside className="entity-card empty">
<h3 className="ec-title">{t('seance.entity.title')}</h3>
<div className="ec-none">
<div className="ec-none-sigil">⟁</div>
<p>{t('seance.entity.none')}</p>
<p className="dim">{t('seance.entity.noneHint')}</p>
<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>
</aside>
</section>
)
}
return (
<aside className="entity-card">
<div className="ec-wax" aria-hidden="true">
{entity.name.slice(0, 1).toUpperCase()}
</div>
<h3 className="ec-title">{t('seance.entity.title')}</h3>
<div className="ec-head">
<GhostGlyph hue={entity.visual.hue} form={entity.visual.form} size={64} />
<div>
<div className="ec-name glitch-hover">{entity.name}</div>
<div className="ec-epithet">{entity.epithet}</div>
<span className={`rarity-badge ${RARITY_CLASS[entity.rarity] ?? ''}`}>
{t(`seance.entity.rarity.${entity.rarity}`)}
</span>
</div>
</div>
<div className="ec-contact">
{isNew ? t('seance.entity.isNew') : t('seance.entity.known', { count: entity.contact_count })}
</div>
<div className="ec-section">
<h4>{t('seance.entity.persona')}</h4>
<p className="ec-persona">{entity.persona}</p>
</div>
{entity.quotes.length > 0 && (
<div className="ec-section">
<h4>{t('seance.entity.quotes')}</h4>
<ul className="ec-quotes">
{entity.quotes.slice(0, 4).map((q, i) => (
<li key={i}>“{q}”</li>
))}
</ul>
<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>
)}
<div className="ec-section">
<h4>{t('seance.entity.voice')}</h4>
<div className="ec-voice">
<span>id {entity.voice.voice_id}</span>
<span>pitch {entity.voice.pitch.toFixed(2)}</span>
<span>rate {entity.voice.rate.toFixed(2)}</span>
<span>noise {entity.voice.noise.toFixed(2)}</span>
<span>echo {entity.voice.echo.toFixed(2)}</span>
</div>
</div>
</aside>
</section>
)
}