Spaces:
Sleeping
Sleeping
Download frontend/src/components/Shell.tsx from misack/war_gaming: direct link, hf CLI and curl.
- Browser
- Download file 5.61 kB
-
https://huggingface.co/spaces/misack/war_gaming/resolve/main/frontend/src/components/Shell.tsx
- Command line
-
hf download hf://spaces/misack/war_gaming/frontend/src/components/Shell.tsx
-
curl -L -o Shell.tsx https://huggingface.co/spaces/misack/war_gaming/resolve/main/frontend/src/components/Shell.tsx
5.61 kB
| import { ReactNode } from 'react'; | |
| import { useI18n, type Language } from '../i18n'; | |
| // Shell components ported from components/Shell.jsx — class names preserved | |
| // so the paper/ink/oxblood Board Console look is identical. | |
| export function TopBar(props: { | |
| game?: { id: string; turn: number; max: number; recon_remaining: number } | null; | |
| route: string; | |
| onNav: (r: string) => void; | |
| }) { | |
| const { game, route, onNav } = props; | |
| const { t, lang, setLang } = useI18n(); | |
| return ( | |
| <header className="nh-topbar"> | |
| <div className="nh-topbar__l"> | |
| <button className="nh-brand" onClick={() => onNav('list')}> | |
| Norhard<em>/</em>War<span className="nh-brand__tag">{t('shell.brand.tag')}</span> | |
| </button> | |
| <nav className="nh-nav"> | |
| <button className={route === 'list' ? 'on' : ''} onClick={() => onNav('list')}> | |
| {t('shell.nav.cases')} | |
| </button> | |
| <button className={route === 'new' ? 'on' : ''} onClick={() => onNav('new')}> | |
| {t('shell.nav.newCase')} | |
| </button> | |
| </nav> | |
| </div> | |
| <div className="nh-topbar__r" style={{ display: 'flex', alignItems: 'center', gap: 12 }}> | |
| {game && ( | |
| <div className="nh-status"> | |
| <span>{t('shell.status.case')} <b className="mono">{game.id.slice(0, 4)}…{game.id.slice(-4)}</b></span> | |
| <span className="nh-sep"></span> | |
| <span>{t('shell.status.turn')} <b>{game.turn} / {game.max}</b></span> | |
| <span className="nh-sep"></span> | |
| <span>{t('shell.status.recon')} <b>{game.recon_remaining} / 3</b></span> | |
| </div> | |
| )} | |
| <LangToggle lang={lang} setLang={setLang} ariaLabel={t('shell.lang.toggle.aria')} /> | |
| </div> | |
| </header> | |
| ); | |
| } | |
| function LangToggle({ | |
| lang, setLang, ariaLabel, | |
| }: { | |
| lang: Language; | |
| setLang: (l: Language) => void; | |
| ariaLabel: string; | |
| }) { | |
| return ( | |
| <div | |
| role="group" | |
| aria-label={ariaLabel} | |
| className="nh-lang" | |
| style={{ | |
| display: 'inline-flex', | |
| gap: 2, | |
| fontFamily: 'var(--font-mono)', | |
| fontSize: 11, | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.06em', | |
| }} | |
| > | |
| <button | |
| type="button" | |
| onClick={() => setLang('en')} | |
| aria-pressed={lang === 'en'} | |
| style={{ | |
| background: 'transparent', | |
| border: 0, | |
| padding: '4px 6px', | |
| cursor: 'pointer', | |
| color: lang === 'en' ? 'var(--ink)' : 'var(--slate)', | |
| fontWeight: lang === 'en' ? 700 : 400, | |
| textDecoration: lang === 'en' ? 'underline' : 'none', | |
| textUnderlineOffset: 3, | |
| fontFamily: 'inherit', | |
| fontSize: 'inherit', | |
| letterSpacing: 'inherit', | |
| textTransform: 'inherit', | |
| }} | |
| > | |
| EN | |
| </button> | |
| <span style={{ color: 'var(--slate)' }}>|</span> | |
| <button | |
| type="button" | |
| onClick={() => setLang('de')} | |
| aria-pressed={lang === 'de'} | |
| style={{ | |
| background: 'transparent', | |
| border: 0, | |
| padding: '4px 6px', | |
| cursor: 'pointer', | |
| color: lang === 'de' ? 'var(--ink)' : 'var(--slate)', | |
| fontWeight: lang === 'de' ? 700 : 400, | |
| textDecoration: lang === 'de' ? 'underline' : 'none', | |
| textUnderlineOffset: 3, | |
| fontFamily: 'inherit', | |
| fontSize: 'inherit', | |
| letterSpacing: 'inherit', | |
| textTransform: 'inherit', | |
| }} | |
| > | |
| DE | |
| </button> | |
| </div> | |
| ); | |
| } | |
| const PHASES: { n: string; key: 'briefing' | 'recon' | 'action' | 'deliberation' | 'adjudication' }[] = [ | |
| { n: '01', key: 'briefing' }, | |
| { n: '02', key: 'recon' }, | |
| { n: '03', key: 'action' }, | |
| { n: '04', key: 'deliberation' }, | |
| { n: '05', key: 'adjudication' }, | |
| ]; | |
| export function PhaseRail({ current }: { current: 'briefing' | 'recon' | 'action' | 'deliberation' | 'adjudication' }) { | |
| const { t } = useI18n(); | |
| const order = ['briefing', 'recon', 'action', 'deliberation', 'adjudication']; | |
| const idx = order.indexOf(current); | |
| return ( | |
| <div className="nh-rail"> | |
| {PHASES.map((p, i) => ( | |
| <div key={p.n} className={`nh-rail__p ${i < idx ? 'done' : ''} ${i === idx ? 'active' : ''}`}> | |
| <span className="n mono">{p.n}</span> | |
| <span className="t">{t(`phaseRail.${p.key}` as const)}</span> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| export function Dossier(props: { | |
| tag?: string; title?: string; children?: ReactNode; footnote?: string[]; | |
| className?: string; fog?: boolean; inverse?: boolean; | |
| }) { | |
| const { tag, title, children, footnote, className = '', fog = false, inverse = false } = props; | |
| const { t } = useI18n(); | |
| return ( | |
| <article className={`nh-dossier ${fog ? 'fog' : ''} ${inverse ? 'inverse' : ''} ${className}`}> | |
| {fog && <span className="nh-dossier__fog">{t('shell.dossier.fog')}</span>} | |
| {tag && <span className="nh-dossier__tag">{tag}</span>} | |
| {title && <h3 className="nh-dossier__h">{title}</h3>} | |
| {children} | |
| {footnote && ( | |
| <div className="nh-dossier__fn"> | |
| {footnote.map((f, i) => <span key={i}>{f}</span>)} | |
| </div> | |
| )} | |
| </article> | |
| ); | |
| } | |
| export function Panel({ tag, title, children }: { tag?: string; title?: string; children: ReactNode }) { | |
| return ( | |
| <section className="nh-panel"> | |
| {tag && <span className="nh-panel__tag">{tag}</span>} | |
| {title && <h3>{title}</h3>} | |
| {children} | |
| </section> | |
| ); | |
| } | |
| export function Spinner() { | |
| return <span className="nh-typing"><i /><i /><i /></span>; | |
| } | |