3. Add a Panel
Goal: Create a new React component, register it in
App.tsx, add i18n translations, and subscribe to data.
Scenario
You want to add a "Latest EI Value" card to the Live page that shows the current Engagement Index in large text.
Step 1: Create the Component
Create src/components/LatestEICard.tsx:
import { useEegStore } from '../store/eegStore';
import type { Locale } from '../i18n';
import { t } from '../i18n';
import { Card, CardHeader, CardBody } from './ui';
interface LatestEICardProps {
locale: Locale;
}
export function LatestEICard({ locale }: LatestEICardProps) {
const smoothEI = useEegStore((s) => s.smoothEngagementResults.at(-1)?.ei ?? null);
return (
<Card>
<CardHeader
eyebrow="Realtime"
title="Latest EI"
/>
<CardBody>
{smoothEI !== null ? (
<p className="font-mono text-[2rem] tabular text-ink">
{smoothEI.toFixed(3)}
</p>
) : (
<p className="text-meta">{t(locale, 'analysis.waiting')}</p>
)}
</CardBody>
</Card>
);
}
Key Points
localeprop: Every panel receiveslocale: Localefor bilingual support.useEegStore: Derives real-time data from Zustand. The selectors.at(-1)gets the latest array element.Card / CardHeader / CardBody: Reusable primitives fromsrc/components/ui/. Always use these for visual consistency.t(locale, key): Avoids hardcoded English strings. If the key doesn't exist yet, add it next.
Step 2: Add i18n Keys
Open src/i18n.ts. Find the en-US section and add:
'latestEi.eyebrow': 'Realtime',
'latestEi.title': 'Latest EI',
Find the zh-CN section and add:
'latestEi.eyebrow': '实时',
'latestEi.title': '最新 EI',
Then update the component to use them:
<CardHeader
eyebrow={t(locale, 'latestEi.eyebrow')}
title={t(locale, 'latestEi.title')}
/>
TypeScript will catch missing or misspelled keys — TranslationKey is a union of all valid key strings.
Step 3: Register in App.tsx
Open src/App.tsx. Import your component at the top:
import { LatestEICard } from './components/LatestEICard';
Find the activePage === 'live' block and insert your card:
{activePage === 'live' && (
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr)_22rem]">
<div className="flex min-w-0 flex-col gap-4">
<LatestEICard locale={locale} /> {/* ← new card */}
<LiveWindowControlPanel locale={locale} />
<BrainHeatmapPanel locale={locale} />
{/* ... existing panels ... */}
</div>
<AiAnalysisSidebar locale={locale} ... />
</div>
)}
Placement Notes
- The Live page has a two-column grid:
xl:grid-cols-[minmax(0,1fr)_22rem] min-w-0is critical on the left column to prevent flex child overflow in Tailwind- The AI sidebar is
xl:sticky— it stays in view while the left column scrolls
Step 4: Verify
npm run typecheck # catches import errors, missing i18n keys
npm run dev # open http://localhost:5173 → Live tab
If npm test passes, your panel is correctly integrated.
Pattern Reference: How Existing Panels Subscribe to Data
| Panel | Data Source | Pattern |
|---|---|---|
AlgorithmTrendPanel | useEegStore(s => s.smoothEngagementResults) | Zustand selector |
RawWaveformPanel | rawWaveformBus.copyLatest() | Observer bus + requestAnimationFrame |
BrainHeatmapPanel | useEegStore(s => s.bandPowerHistory) | Zustand selector |
AiAnalysisSidebar | useAiStore(s => s.analysisOutput) | Zustand selector |
Rule of thumb:
- If your panel updates at display rate with 250 Hz data → use observer bus
- If your panel updates at analysis rate (every 0.5s or slower) → use Zustand
Common Mistakes
- Forgetting
localeprop: Every panel must acceptlocale: Localeand pass it tot(). - Hardcoding strings: Always use
t(locale, 'key'). If the key doesn't exist, add it toi18n.ts. - Not wrapping in
Card: All panels should useCard / CardHeader / CardBodyfor visual consistency. - Over-subscribing in Zustand: Select only what you need.
useEegStore(s => s.smoothEngagementResults)is good;useEegStore()is bad (re-renders on every state change).