app: rebuild interface base on scificn-ui Mission Control layout (placeholder content)
CI / build + unit + e2e (pull_request) Failing after 13m21s

This commit is contained in:
2026-09-28 16:13:35 +00:00
parent 6d267fba6b
commit 531798d4b3
14 changed files with 1992 additions and 83 deletions
+653
View File
@@ -0,0 +1,653 @@
'use client'
import { useEffect, useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { BarChart } from '@/components/ui/bar-chart'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { LineChart } from '@/components/ui/line-chart'
import { Panel, PanelContent, PanelHeader, PanelTitle } from '@/components/ui/panel'
import { ProgressRing } from '@/components/ui/progress-ring'
import { StatCard } from '@/components/ui/stat-card'
import { StatusGrid, type SystemEntry } from '@/components/ui/status-grid'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Terminal, type TerminalLine } from '@/components/ui/terminal'
// Interface base: layout + visual treatment rebuilt from the scificn-ui Mission Control
// showcase (src/app/_pages/showcase/mission-control-content.tsx in baxy5/scificn-ui), with
// placeholder SpaceWanderer content — no game logic yet. Single player, no auth, by design.
function useNarrow(threshold = 900): boolean {
const [narrow, setNarrow] = useState(
() => typeof window !== 'undefined' && window.innerWidth < threshold
)
useEffect(() => {
const handle = () => setNarrow(window.innerWidth < threshold)
window.addEventListener('resize', handle, { passive: true })
return () => window.removeEventListener('resize', handle)
}, [threshold])
return narrow
}
function now(): string {
return new Date().toLocaleTimeString('en-GB', { hour12: false })
}
// ── Placeholder content (no game logic — real values land with the turn engine) ──
const shipSystems: SystemEntry[] = [
{ name: 'REACTOR', status: 'ACTIVE', detail: '94.2%' },
{ name: 'LIFE SUPPORT', status: 'ACTIVE' },
{ name: 'NAV COMPUTER', status: 'SCANNING' },
{ name: 'WEAPONS', status: 'OFFLINE' },
{ name: 'COMMS', status: 'ACTIVE' },
]
const bayOutput = [
{ label: 'PWR', value: 14 },
{ label: 'NAV', value: 9 },
{ label: 'COM', value: 6 },
{ label: 'WPN', value: 3 },
{ label: 'LSS', value: 12 },
{ label: 'ENG', value: 11 },
{ label: 'BAY', value: 7 },
{ label: 'MED', value: 5 },
]
const hullTrack = [100, 98, 97, 96, 94, 95, 92, 90, 89, 87]
const hullLabels = hullTrack.map((_, i) => `T-${hullTrack.length - 1 - i}`)
const encounters = [
{ name: 'DERELICT FREIGHTER', when: 'T-07:14', id: '#E-102' },
{ name: 'PIRATE SKIRMISH', when: 'T-22:40', id: '#E-103' },
{ name: 'ANOMALY FIELD', when: 'T-48:12', id: '#E-104' },
]
const shipData = [
{ label: 'TOTAL MODULES', value: '34' },
{ label: 'HULL INTEGRITY', value: '87%' },
{ label: 'REACTOR OUTPUT', value: '94.2%' },
{ label: 'CREW ACTIVE', value: '3 / 6' },
{ label: 'SECTOR', value: '7G // DRIFT LANE' },
{ label: 'DRIFT VELOCITY', value: '0.4 KM/S' },
{ label: 'TURN', value: '12' },
]
const logLines: TerminalLine[] = [
{ type: 'system', text: 'SPACEWANDERER // INITIALIZING SHIP SYSTEMS...', timestamp: now() },
{ type: 'output', text: 'HULL SCAN COMPLETE — INTEGRITY 87%', timestamp: now() },
{ type: 'output', text: 'REACTOR OUTPUT STABLE AT 94.2%', timestamp: now() },
{ type: 'output', text: 'CREW MANIFEST LOADED — 3 ACTIVE PERSONNEL', timestamp: now() },
{ type: 'warn', text: 'WEAPONS OFFLINE — MODULE NOT INSTALLED', timestamp: now() },
{ type: 'system', text: 'ALL SYSTEMS NOMINAL. AWAITING FIRST TURN.', timestamp: now() },
]
export function MissionControl() {
const narrow = useNarrow()
const [tick, setTick] = useState(0)
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1000)
return () => clearInterval(id)
}, [])
const timeStr = new Date().toLocaleTimeString('en-GB', { hour12: false })
return (
<div
style={{
minHeight: '100vh',
background: 'var(--background)',
display: 'flex',
flexDirection: 'column',
fontFamily: 'var(--font-mono)',
}}
>
{/* ── HEADER ── */}
<header
style={{
height: '44px',
background: 'var(--surface)',
borderBottom: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
padding: '0 1.25rem',
gap: '0.75rem',
position: 'sticky',
top: 0,
zIndex: 50,
flexShrink: 0,
}}
>
<span
style={{
color: 'var(--text-muted)',
fontSize: '0.7rem',
letterSpacing: '0.08em',
flexShrink: 0,
}}
>
◄ SHIP
</span>
<span style={{ color: 'var(--border)', flexShrink: 0 }}>│</span>
<h1
style={{
margin: 0,
color: 'var(--color-green)',
fontSize: '0.85rem',
fontWeight: 700,
letterSpacing: '0.12em',
textShadow: 'var(--text-glow-green)',
flexShrink: 0,
}}
>
SPACEWANDERER
</h1>
{!narrow && (
<span
style={{
color: 'var(--text-muted)',
fontSize: '0.72rem',
letterSpacing: '0.08em',
flexShrink: 0,
}}
>
// SHIP STATUS // CREW MANIFEST
</span>
)}
<div style={{ flex: 1 }} />
{!narrow && (
<Badge variant="ACTIVE">SHIP NOMINAL</Badge>
)}
<Badge variant="SCANNING">SECTOR SCAN</Badge>
<span
style={{
color: 'var(--color-green)',
fontSize: '0.65rem',
letterSpacing: '0.1em',
textShadow: 'var(--text-glow-green)',
flexShrink: 0,
opacity: tick % 2 === 0 ? 1 : 0.5,
transition: 'opacity 0.3s',
}}
>
{timeStr}
</span>
</header>
{/* ── BODY ── */}
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: '1px',
background: 'var(--border)',
}}
>
{/* STAT CARDS — placeholder ship readouts */}
<div
style={{
background: 'var(--background)',
padding: '0.75rem 1rem',
display: 'grid',
gridTemplateColumns: narrow ? '1fr 1fr' : 'repeat(4, 1fr)',
gap: '0.75rem',
}}
>
<StatCard
label="HULL INTEGRITY"
value="87%"
sublabel="NOMINAL · NO BREACHES"
variant="ACTIVE"
/>
<StatCard
label="REACTOR OUTPUT"
value="94.2%"
sublabel="STABLE · IDLE CYCLE"
variant="ACTIVE"
/>
<StatCard
label="CREW ON BOARD"
value="3 / 6"
sublabel="AWAITING MANIFEST"
variant="DEFAULT"
/>
<StatCard
label="NEXT ENCOUNTER"
value="T-07:14"
sublabel="SECTOR 7G // DRIFT LANE"
variant="WARNING"
/>
</div>
{/* 3-COLUMN GRID */}
<div
style={{
flex: 1,
display: 'grid',
gridTemplateColumns: narrow ? '1fr' : '260px 1fr 240px',
gap: '1px',
background: 'var(--border)',
}}
>
{/* ── LEFT: SHIP SYSTEMS + VESSEL METRICS ── */}
<div
style={{
background: 'var(--background)',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.875rem',
overflowY: 'auto',
}}
>
<Panel notch="sm">
<PanelHeader>
<PanelTitle>SHIP SYSTEMS</PanelTitle>
<div style={{ marginLeft: 'auto' }}>
<Badge variant="ACTIVE">4 ONLINE</Badge>
</div>
</PanelHeader>
<StatusGrid systems={shipSystems} />
</Panel>
<Panel notch="sm">
<PanelHeader>
<PanelTitle>VESSEL METRICS</PanelTitle>
</PanelHeader>
<PanelContent>
<div
style={{
display: 'flex',
justifyContent: 'space-around',
paddingTop: '0.5rem',
paddingBottom: '0.25rem',
}}
>
<ProgressRing value={87} label="HULL" variant="ACTIVE" size={74} />
<ProgressRing value={94} label="REACTOR" variant="ACTIVE" size={74} />
<ProgressRing value={50} label="CREW / 6" variant="WARNING" size={74} />
</div>
<div
style={{
marginTop: '0.75rem',
paddingTop: '0.75rem',
borderTop: '1px solid var(--border)',
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
gap: '0.25rem',
textAlign: 'center',
}}
>
{[
{ label: 'HULL INTEGRITY', note: 'last scan T-02' },
{ label: 'REACTOR OUTPUT', note: 'idle cycle' },
{ label: 'CREW ROSTER', note: 'of 6 berths' },
].map((item) => (
<div key={item.label}>
<div
style={{
fontSize: '0.52rem',
color: 'var(--text-muted)',
letterSpacing: '0.08em',
textTransform: 'uppercase',
}}
>
{item.label}
</div>
<div
style={{
fontSize: '0.48rem',
color: 'var(--border)',
letterSpacing: '0.06em',
marginTop: '1px',
}}
>
{item.note}
</div>
</div>
))}
</div>
</PanelContent>
</Panel>
</div>
{/* ── CENTER: OPERATIONS TABS ── */}
<div
style={{
background: 'var(--background)',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.875rem',
overflowY: 'auto',
}}
>
<Panel notch="md" style={{ flex: 1 }}>
<PanelHeader>
<PanelTitle>OPERATIONS // SHIP COMMAND</PanelTitle>
<div style={{ marginLeft: 'auto', display: 'flex', gap: '0.5rem' }}>
<Badge variant="ACTIVE">SYS NOMINAL</Badge>
</div>
</PanelHeader>
<PanelContent style={{ padding: 0 }}>
<Tabs defaultValue="systems">
<TabsList
style={{ padding: '0 1rem', background: 'var(--surface-raised)' }}
>
<TabsTrigger value="systems">SYSTEMS</TabsTrigger>
<TabsTrigger value="next">NEXT MISSION</TabsTrigger>
<TabsTrigger value="log">LOG</TabsTrigger>
<TabsTrigger value="hull">HULL TRACK</TabsTrigger>
</TabsList>
{/* SYSTEMS TAB */}
<TabsContent value="systems" style={{ margin: 0, padding: '1rem' }}>
<BarChart
data={bayOutput}
orientation="vertical"
variant="ACTIVE"
title="MODULE OUTPUT BY BAY (PLACEHOLDER)"
style={{ width: '100%' }}
/>
<div
style={{
marginTop: '1rem',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '0.625rem',
}}
>
{[
{ label: 'TOTAL MODULES', value: 34 },
{ label: 'ONLINE', value: 29 },
{ label: 'EFFICIENCY', value: '85%' },
].map((s) => (
<div
key={s.label}
style={{
padding: '0.75rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
textAlign: 'center',
}}
>
<div
style={{
fontSize: '0.55rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
marginBottom: '0.4rem',
}}
>
{s.label}
</div>
<div
style={{
fontSize: '1.1rem',
fontWeight: 700,
color: 'var(--color-green)',
textShadow: 'var(--text-glow-green)',
letterSpacing: '0.02em',
}}
>
{s.value}
</div>
</div>
))}
</div>
</TabsContent>
{/* NEXT MISSION TAB */}
<TabsContent value="next" style={{ margin: 0, padding: '1rem' }}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}
>
<Alert variant="STATUS">
<AlertTitle>ENCOUNTER #E-102 — DERELICT FREIGHTER</AlertTitle>
<AlertDescription>
Placeholder briefing. Real encounter resolution lands with the turn engine.
</AlertDescription>
</Alert>
<div
style={{
display: 'grid',
gridTemplateColumns: narrow ? '1fr' : '1fr 1fr',
gap: '0.625rem',
}}
>
{[
{ label: 'DESTINATION', value: 'SECTOR 7G // DRIFT LANE' },
{ label: 'ETA', value: 'T-07:14 (PLACEHOLDER)' },
{ label: 'THREAT LEVEL', value: 'LOW — UNMANNED' },
{ label: 'CARGO MANIFEST', value: 'UNKNOWN' },
].map((item) => (
<div
key={item.label}
style={{
padding: '0.75rem',
background: 'var(--surface)',
border: '1px solid var(--border)',
}}
>
<div
style={{
fontSize: '0.55rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
marginBottom: '0.35rem',
}}
>
{item.label}
</div>
<div
style={{
fontSize: '0.8rem',
color: 'var(--text-secondary)',
letterSpacing: '0.04em',
}}
>
{item.value}
</div>
</div>
))}
</div>
</div>
</TabsContent>
{/* LOG TAB */}
<TabsContent value="log" style={{ margin: 0, padding: '1rem' }}>
<Terminal
lines={logLines}
title="SHIP LOG // PLACEHOLDER FEED"
height="22rem"
style={{ width: '100%' }}
/>
</TabsContent>
{/* HULL TRACK TAB */}
<TabsContent value="hull" style={{ margin: 0, padding: '1rem' }}>
<LineChart
series={[{ id: 'hull', label: 'HULL INTEGRITY %', data: hullTrack }]}
labels={hullLabels}
title="HULL INTEGRITY TRACK // PLACEHOLDER"
showArea
animated={false}
style={{ width: '100%' }}
/>
</TabsContent>
</Tabs>
</PanelContent>
</Panel>
</div>
{/* ── RIGHT: UPCOMING ENCOUNTERS + SHIP DATA ── */}
<div
style={{
background: 'var(--background)',
padding: '1rem',
display: 'flex',
flexDirection: 'column',
gap: '0.875rem',
overflowY: 'auto',
}}
>
<Panel notch="sm">
<PanelHeader>
<PanelTitle>UPCOMING ENCOUNTERS</PanelTitle>
<div style={{ marginLeft: 'auto' }}>
<Badge variant="SCANNING">{encounters.length} QUEUED</Badge>
</div>
</PanelHeader>
<PanelContent style={{ padding: 0 }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
{encounters.map((enc, i) => {
const isNext = i === 0
const isLast = i === encounters.length - 1
return (
<div
key={enc.id}
style={{
padding: '0.6rem 0.75rem',
borderBottom: isLast ? 'none' : '1px solid var(--border)',
background: isNext ? 'var(--surface-raised)' : 'transparent',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '0.5rem',
}}
>
<div style={{ overflow: 'hidden', flex: 1 }}>
<div
style={{
fontSize: '0.68rem',
color: isNext ? 'var(--text-secondary)' : 'var(--text-muted)',
letterSpacing: '0.04em',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
textTransform: 'uppercase',
}}
>
{enc.name}
</div>
<div
style={{
fontSize: '0.57rem',
color: 'var(--text-muted)',
letterSpacing: '0.06em',
marginTop: '2px',
}}
>
{enc.when} // {enc.id}
</div>
</div>
<Badge variant={isNext ? 'WARNING' : 'SCANNING'}>
{isNext ? 'NEXT' : 'SCHED'}
</Badge>
</div>
</div>
)
})}
</div>
</PanelContent>
</Panel>
<Panel notch="sm">
<PanelHeader>
<PanelTitle>SHIP DATA</PanelTitle>
</PanelHeader>
<PanelContent>
<div style={{ display: 'flex', flexDirection: 'column' }}>
{shipData.map((item, i) => (
<div
key={item.label}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.4rem 0',
borderBottom:
i < shipData.length - 1 ? '1px solid var(--border)' : 'none',
}}
>
<span
style={{
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.1em',
}}
>
{item.label}
</span>
<span
style={{
fontSize: '0.78rem',
color: 'var(--color-green)',
textShadow: 'var(--text-glow-green)',
letterSpacing: '0.04em',
fontWeight: 600,
}}
>
{item.value}
</span>
</div>
))}
</div>
</PanelContent>
</Panel>
</div>
</div>
</div>
{/* ── FOOTER ── */}
<footer
style={{
minHeight: '44px',
background: 'var(--surface)',
borderTop: '1px solid var(--border)',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
padding: '0.5rem 1.25rem',
gap: '0.5rem',
position: 'sticky',
bottom: 0,
zIndex: 50,
flexShrink: 0,
}}
>
<span style={{ fontSize: '0.6rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
INTERFACE BASE: SCIFICN MISSION CONTROL
</span>
<span style={{ color: 'var(--border)' }}>│</span>
<span style={{ fontSize: '0.6rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
CONTENT: PLACEHOLDER — NO GAME LOGIC YET
</span>
<span style={{ color: 'var(--border)' }}>│</span>
<span style={{ fontSize: '0.6rem', color: 'var(--text-muted)', letterSpacing: '0.08em' }}>
SINGLE PLAYER // NO AUTH
</span>
<div style={{ flex: 1 }} />
<Badge variant="ACTIVE">SYS ONLINE</Badge>
</footer>
</div>
)
}
+257
View File
@@ -0,0 +1,257 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export type BarChartVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL'
export interface BarChartEntry {
label: string
value: number
}
export interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
data: BarChartEntry[]
orientation?: 'horizontal' | 'vertical'
variant?: BarChartVariant
title?: string
showValues?: boolean
/** Override the max value for scaling. Defaults to max(data). */
max?: number
}
const VERTICAL_CHART_HEIGHT = 144 // px — 9rem
const BarChart = React.forwardRef<HTMLDivElement, BarChartProps>(
(
{
className,
data,
orientation = 'horizontal',
variant = 'ACTIVE',
title,
showValues = true,
max,
style,
...props
},
ref
) => {
const computedMax = max ?? Math.max(...data.map((d) => d.value), 1)
const accentColor =
variant === 'ACTIVE' ? 'var(--color-green)' :
variant === 'WARNING' ? 'var(--color-amber)' :
variant === 'CRITICAL' ? 'var(--color-red)' :
'var(--text-secondary)'
const accentGlow =
variant === 'ACTIVE' ? 'var(--glow-green)' :
variant === 'WARNING' ? 'var(--glow-amber)' :
variant === 'CRITICAL' ? 'var(--glow-red)' :
'none'
return (
<div
ref={ref}
className={cn(className)}
style={{
border: '1px solid var(--border)',
background: 'var(--surface)',
fontFamily: 'var(--font-mono)',
overflow: 'hidden',
...style,
}}
{...props}
>
{/* Title bar */}
{title && (
<div
style={{
padding: '0.4rem 0.75rem',
borderBottom: '1px solid var(--border)',
background: 'var(--surface-raised)',
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}
>
{title}
</div>
)}
{orientation === 'horizontal' ? (
/* ── Horizontal bars ── */
<div
style={{
padding: '0.75rem',
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{data.map((entry, i) => {
const pct = Math.max((entry.value / computedMax) * 100, 0)
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
{/* Label */}
<span
style={{
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.06em',
textTransform: 'uppercase',
width: 'clamp(3rem, 20%, 7rem)',
flexShrink: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{entry.label}
</span>
{/* Track */}
<div
style={{
flex: 1,
height: '0.65rem',
background: 'var(--surface-raised)',
border: '1px solid var(--border)',
position: 'relative',
overflow: 'hidden',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
width: `${pct}%`,
background: accentColor,
boxShadow: accentGlow,
opacity: 0.85,
transition: 'width 0.4s ease',
}}
/>
</div>
{/* Value */}
{showValues && (
<span
style={{
fontSize: '0.6rem',
color: accentColor,
letterSpacing: '0.04em',
width: '3rem',
textAlign: 'right',
flexShrink: 0,
}}
>
{entry.value}
</span>
)}
</div>
)
})}
</div>
) : (
/* ── Vertical bars ── */
<div>
{/* Bar area */}
<div
style={{
display: 'flex',
alignItems: 'flex-end',
gap: '0.4rem',
height: VERTICAL_CHART_HEIGHT,
padding: '0.75rem 0.75rem 0',
}}
>
{data.map((entry, i) => {
const pct = Math.max((entry.value / computedMax) * 100, 0)
const barPx = Math.round((pct / 100) * VERTICAL_CHART_HEIGHT)
return (
<div
key={i}
style={{
flex: 1,
height: VERTICAL_CHART_HEIGHT,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'flex-end',
gap: '0.2rem',
}}
>
{/* Value above bar */}
{showValues && (
<span
style={{
fontSize: '0.5rem',
color: accentColor,
letterSpacing: '0.04em',
flexShrink: 0,
}}
>
{entry.value}
</span>
)}
{/* Bar */}
<div
style={{
width: '100%',
height: barPx > 0 ? barPx : 0,
minHeight: entry.value > 0 ? 2 : 0,
background: accentColor,
boxShadow: accentGlow,
opacity: 0.85,
flexShrink: 0,
transition: 'height 0.4s ease',
}}
/>
</div>
)
})}
</div>
{/* Labels row */}
<div
style={{
display: 'flex',
gap: '0.4rem',
padding: '0.25rem 0.75rem 0.75rem',
borderTop: '1px solid var(--border)',
marginTop: '0.25rem',
}}
>
{data.map((entry, i) => (
<div
key={i}
style={{
flex: 1,
textAlign: 'center',
fontSize: '0.5rem',
color: 'var(--text-muted)',
letterSpacing: '0.06em',
textTransform: 'uppercase',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{entry.label}
</div>
))}
</div>
</div>
)}
</div>
)
}
)
BarChart.displayName = 'BarChart'
export { BarChart }
+312
View File
@@ -0,0 +1,312 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
export type LineChartVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL'
export interface LineChartSeries {
id: string
label: string
data: number[]
variant?: LineChartVariant
}
export interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
series: LineChartSeries[]
labels: string[]
title?: string
height?: number
min?: number
max?: number
showDots?: boolean
showArea?: boolean
showGrid?: boolean
showLegend?: boolean
animated?: boolean
}
const LEFT = 40
const RIGHT = 8
const TOP = 8
const BOTTOM = 24
const SERIES_COLORS = [
'var(--color-green)',
'var(--color-amber)',
'var(--color-red)',
'var(--color-blue)',
]
const VARIANT_COLORS: Record<LineChartVariant, string> = {
DEFAULT: 'var(--text-secondary)',
ACTIVE: 'var(--color-green)',
WARNING: 'var(--color-amber)',
CRITICAL: 'var(--color-red)',
}
function seriesColor(s: LineChartSeries, idx: number): string {
if (s.variant) return VARIANT_COLORS[s.variant]
return SERIES_COLORS[idx % SERIES_COLORS.length]
}
const LineChart = React.forwardRef<HTMLDivElement, LineChartProps>(
(
{
className,
series,
labels,
title,
height = 160,
min,
max,
showDots = true,
showArea = false,
showGrid = true,
showLegend,
animated = true,
style,
...props
},
ref
) => {
const containerRef = React.useRef<HTMLDivElement>(null)
const [svgW, setSvgW] = React.useState(500)
const [drawn, setDrawn] = React.useState(false)
React.useEffect(() => {
if (!containerRef.current) return
const ro = new ResizeObserver(entries => {
setSvgW(entries[0].contentRect.width)
})
ro.observe(containerRef.current)
return () => ro.disconnect()
}, [])
React.useEffect(() => {
const id = requestAnimationFrame(() => setDrawn(true))
return () => cancelAnimationFrame(id)
}, [])
const allValues = series.flatMap(s => s.data)
const dataMin = allValues.length > 0 ? Math.min(...allValues) : 0
const dataMax = allValues.length > 0 ? Math.max(...allValues) : 100
const minV = min ?? dataMin
const maxV = max ?? dataMax
const range = Math.max(maxV - minV, 1)
const plotW = svgW - LEFT - RIGHT
const plotH = height - TOP - BOTTOM
const n = labels.length
function xPos(i: number) {
return LEFT + (n <= 1 ? plotW / 2 : (i / (n - 1)) * plotW)
}
function yPos(v: number) {
return TOP + (1 - (v - minV) / range) * plotH
}
function buildLinePath(data: number[]): string {
return data
.map((v, i) => `${i === 0 ? 'M' : 'L'} ${xPos(i).toFixed(1)} ${yPos(v).toFixed(1)}`)
.join(' ')
}
function buildAreaPath(data: number[]): string {
if (data.length === 0) return ''
const line = buildLinePath(data)
const firstX = xPos(0).toFixed(1)
const lastX = xPos(data.length - 1).toFixed(1)
const bottomY = (TOP + plotH).toFixed(1)
return `${line} L ${lastX} ${bottomY} L ${firstX} ${bottomY} Z`
}
const gridFracs = [0, 1 / 3, 2 / 3, 1]
const showLeg = showLegend ?? series.length > 1
const step = Math.max(1, Math.ceil(n / 8))
const shouldAnimate = animated
return (
<div
ref={ref}
className={cn(className)}
style={{
border: '1px solid var(--border)',
background: 'var(--surface)',
fontFamily: 'var(--font-mono)',
overflow: 'hidden',
...style,
}}
{...props}
>
{title && (
<div
style={{
padding: '0.4rem 0.75rem',
borderBottom: '1px solid var(--border)',
background: 'var(--surface-raised)',
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}
>
{title}
</div>
)}
<div ref={containerRef} style={{ width: '100%', height: `${height}px` }}>
<svg width={svgW} height={height} style={{ display: 'block' }}>
{/* Grid */}
{showGrid && gridFracs.map((f, gi) => {
const y = TOP + f * plotH
const val = minV + (1 - f) * range
return (
<g key={gi}>
<line
x1={LEFT}
y1={y.toFixed(1)}
x2={(LEFT + plotW).toFixed(1)}
y2={y.toFixed(1)}
stroke="var(--border)"
strokeWidth={0.5}
/>
<text
x={LEFT - 4}
y={y}
textAnchor="end"
dominantBaseline="middle"
fontSize={9}
fill="var(--text-muted)"
fontFamily="var(--font-mono)"
>
{val.toFixed(0)}
</text>
</g>
)
})}
{/* X-axis labels */}
{labels.map((label, i) => {
if (i % step !== 0 && i !== n - 1) return null
return (
<text
key={i}
x={xPos(i)}
y={TOP + plotH + 14}
textAnchor="middle"
fontSize={9}
fill="var(--text-muted)"
fontFamily="var(--font-mono)"
>
{label}
</text>
)
})}
{/* Series */}
{series.map((s, si) => {
if (s.data.length === 0) return null
const color = seriesColor(s, si)
const linePath = buildLinePath(s.data)
const areaPath = buildAreaPath(s.data)
return (
<g key={s.id}>
{/* Area fill */}
{showArea && (
<path d={areaPath} fill={color} opacity={0.1} />
)}
{/* Glow layer */}
<path
d={linePath}
fill="none"
stroke={color}
strokeWidth={6}
opacity={0.12}
pathLength={100}
strokeDasharray={100}
strokeDashoffset={shouldAnimate ? (drawn ? 0 : 100) : 0}
style={shouldAnimate ? { transition: 'stroke-dashoffset 0.8s ease' } : undefined}
/>
{/* Main line */}
<path
d={linePath}
fill="none"
stroke={color}
strokeWidth={1.5}
pathLength={100}
strokeDasharray={100}
strokeDashoffset={shouldAnimate ? (drawn ? 0 : 100) : 0}
style={shouldAnimate ? { transition: 'stroke-dashoffset 0.8s ease' } : undefined}
/>
{/* Dots */}
{showDots && s.data.map((v, i) => (
<circle
key={i}
cx={xPos(i)}
cy={yPos(v)}
r={3}
fill={color}
opacity={shouldAnimate ? (drawn ? 1 : 0) : 1}
style={shouldAnimate ? { transition: 'opacity 0.8s ease 0.4s' } : undefined}
/>
))}
</g>
)
})}
</svg>
</div>
{/* Legend */}
{showLeg && (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.75rem',
padding: '0.5rem 0.75rem',
borderTop: '1px solid var(--border)',
background: 'var(--surface-raised)',
}}
>
{series.map((s, si) => {
const color = seriesColor(s, si)
return (
<div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
<div
style={{
width: '0.5rem',
height: '0.5rem',
borderRadius: '50%',
background: color,
flexShrink: 0,
}}
/>
<span
style={{
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.06em',
textTransform: 'uppercase',
}}
>
{s.label}
</span>
</div>
)
})}
</div>
)}
</div>
)
}
)
LineChart.displayName = 'LineChart'
export { LineChart }
+158
View File
@@ -0,0 +1,158 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export type ProgressRingVariant = 'DEFAULT' | 'ACTIVE' | 'WARNING' | 'CRITICAL'
export interface ProgressRingProps extends React.HTMLAttributes<HTMLDivElement> {
value: number
size?: number
strokeWidth?: number
label?: string
showValue?: boolean
variant?: ProgressRingVariant
}
const ProgressRing = React.forwardRef<HTMLDivElement, ProgressRingProps>(
(
{
className,
value = 0,
size = 120,
strokeWidth = 6,
label,
showValue = true,
variant = 'DEFAULT',
style,
...props
},
ref
) => {
const pct = Math.min(Math.max(value, 0), 100)
const cx = size / 2
const cy = size / 2
const r = (size - strokeWidth) / 2
const circ = 2 * Math.PI * r
const offset = circ * (1 - pct / 100)
const accentColor =
variant === 'ACTIVE' ? 'var(--color-green)' :
variant === 'WARNING' ? 'var(--color-amber)' :
variant === 'CRITICAL' ? 'var(--color-red)' :
'var(--text-secondary)'
const textGlow =
variant === 'ACTIVE' ? 'var(--text-glow-green)' :
variant === 'WARNING' ? 'var(--text-glow-amber)' :
variant === 'CRITICAL' ? 'var(--text-glow-red)' :
'none'
const isSmall = size < 80
return (
<div
ref={ref}
className={cn(className)}
style={{
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: size,
height: size,
fontFamily: 'var(--font-mono)',
flexShrink: 0,
...style,
}}
{...props}
>
<svg
width={size}
height={size}
style={{ position: 'absolute', inset: 0 }}
aria-hidden="true"
>
{/* Track ring */}
<circle
cx={cx} cy={cy} r={r}
fill="none"
stroke="var(--border)"
strokeWidth={strokeWidth}
/>
{/* Glow layer — wider, low opacity */}
{pct > 0 && (
<circle
cx={cx} cy={cy} r={r}
fill="none"
stroke={accentColor}
strokeWidth={strokeWidth + 8}
strokeDasharray={circ}
strokeDashoffset={offset}
strokeLinecap="butt"
transform={`rotate(-90 ${cx} ${cy})`}
opacity={0.12}
/>
)}
{/* Main arc */}
<circle
cx={cx} cy={cy} r={r}
fill="none"
stroke={accentColor}
strokeWidth={strokeWidth}
strokeDasharray={circ}
strokeDashoffset={offset}
strokeLinecap="butt"
transform={`rotate(-90 ${cx} ${cy})`}
style={{ transition: 'stroke-dashoffset 0.4s ease' }}
/>
</svg>
{/* Center label */}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.1rem',
pointerEvents: 'none',
}}
>
{showValue && (
<span
style={{
fontSize: isSmall ? '0.7rem' : '1.2rem',
fontWeight: 700,
color: accentColor,
textShadow: textGlow,
lineHeight: 1,
letterSpacing: '-0.02em',
}}
>
{Math.round(pct)}%
</span>
)}
{label && (
<span
style={{
fontSize: isSmall ? '0.45rem' : '0.55rem',
color: 'var(--text-muted)',
letterSpacing: '0.08em',
textTransform: 'uppercase',
textAlign: 'center',
maxWidth: r * 1.2,
lineHeight: 1.2,
}}
>
{label}
</span>
)}
</div>
</div>
)
}
)
ProgressRing.displayName = 'ProgressRing'
export { ProgressRing }
+146
View File
@@ -0,0 +1,146 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const statCardVariants = cva(
[
'relative flex flex-col font-mono',
'border border-solid rounded-none',
],
{
variants: {
variant: {
DEFAULT: ['border-[var(--border)]'],
ACTIVE: ['border-[var(--color-green)]'],
WARNING: ['border-[var(--color-amber)]'],
CRITICAL: ['border-[var(--color-red)]'],
},
},
defaultVariants: { variant: 'DEFAULT' },
}
)
export interface StatCardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof statCardVariants> {
label: string
value: string | number
/** Delta text, e.g. "+2.3%" */
delta?: string
/** true = positive (▲ green), false = negative (▼ red), undefined = neutral */
deltaPositive?: boolean
/** Small status line shown at the bottom of the card */
sublabel?: string
}
const StatCard = React.forwardRef<HTMLDivElement, StatCardProps>(
(
{
className,
variant = 'DEFAULT',
label,
value,
delta,
deltaPositive,
sublabel,
style,
...props
},
ref
) => {
const accentColor =
variant === 'ACTIVE' ? 'var(--color-green)' :
variant === 'WARNING' ? 'var(--color-amber)' :
variant === 'CRITICAL' ? 'var(--color-red)' :
'var(--text-secondary)'
const accentGlow =
variant === 'ACTIVE' ? 'var(--text-glow-green)' :
variant === 'WARNING' ? 'var(--text-glow-amber)' :
variant === 'CRITICAL' ? 'var(--text-glow-red)' :
'none'
const deltaColor =
deltaPositive === true ? 'var(--color-green)' :
deltaPositive === false ? 'var(--color-red)' :
'var(--text-muted)'
const deltaSymbol =
deltaPositive === true ? '▲ ' :
deltaPositive === false ? '▼ ' :
'— '
return (
<div
ref={ref}
className={cn(statCardVariants({ variant }), className)}
style={{
padding: '1rem',
gap: '0.4rem',
background: 'var(--surface)',
...style,
}}
{...props}
>
{/* Label */}
<div
style={{
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}
>
{label}
</div>
{/* Value */}
<div
style={{
fontSize: '2rem',
fontWeight: 700,
color: accentColor,
textShadow: accentGlow,
lineHeight: 1,
letterSpacing: '-0.02em',
}}
>
{value}
</div>
{/* Delta */}
{delta !== undefined && (
<div
style={{
fontSize: '0.7rem',
color: deltaColor,
letterSpacing: '0.05em',
}}
>
{deltaSymbol}{delta}
</div>
)}
{/* Sublabel */}
{sublabel && (
<div
style={{
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.1em',
textTransform: 'uppercase',
marginTop: 'auto',
paddingTop: '0.6rem',
borderTop: '1px solid var(--border)',
}}
>
{sublabel}
</div>
)}
</div>
)
}
)
StatCard.displayName = 'StatCard'
export { StatCard, statCardVariants }
+158
View File
@@ -0,0 +1,158 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export type SystemStatus = 'ACTIVE' | 'OFFLINE' | 'WARNING' | 'CRITICAL' | 'SCANNING'
export interface SystemEntry {
name: string
status: SystemStatus
/** Optional secondary text shown beside the name */
detail?: string
}
export interface StatusGridProps extends React.HTMLAttributes<HTMLDivElement> {
systems: SystemEntry[]
title?: string
columns?: 1 | 2 | 3
}
function statusColor(s: SystemStatus): string {
switch (s) {
case 'ACTIVE': return 'var(--color-green)'
case 'OFFLINE': return 'var(--text-muted)'
case 'WARNING': return 'var(--color-amber)'
case 'CRITICAL': return 'var(--color-red)'
case 'SCANNING': return 'var(--color-green)'
}
}
function statusGlow(s: SystemStatus): string {
switch (s) {
case 'ACTIVE': return 'var(--glow-green)'
case 'SCANNING': return 'var(--glow-green)'
case 'WARNING': return 'var(--glow-amber)'
case 'CRITICAL': return 'var(--glow-red)'
default: return 'none'
}
}
const StatusGrid = React.forwardRef<HTMLDivElement, StatusGridProps>(
({ className, systems, title, columns = 1, style, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(className)}
style={{
border: '1px solid var(--border)',
background: 'var(--surface)',
fontFamily: 'var(--font-mono)',
overflow: 'hidden',
...style,
}}
{...props}
>
{/* Header */}
{title && (
<div
style={{
padding: '0.4rem 0.75rem',
borderBottom: '1px solid var(--border)',
background: 'var(--surface-raised)',
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}
>
{title}
</div>
)}
{/* Grid of rows */}
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
}}
>
{systems.map((system, i) => {
const color = statusColor(system.status)
const glow = statusGlow(system.status)
const isScanning = system.status === 'SCANNING'
// border logic: no bottom border on last row(s), no right on last column
const totalRows = Math.ceil(systems.length / columns)
const rowIndex = Math.floor(i / columns)
const colIndex = i % columns
const isLastRow = rowIndex === totalRows - 1
const isLastCol = colIndex === columns - 1
return (
<div
key={i}
style={{
display: 'grid',
gridTemplateColumns: '1fr auto',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
borderBottom: isLastRow ? 'none' : '1px solid var(--border)',
borderRight: isLastCol ? 'none' : '1px solid var(--border)',
minWidth: 0,
}}
>
{/* Name + optional detail */}
<span
style={{
minWidth: 0,
fontSize: '0.7rem',
color: 'var(--text-secondary)',
letterSpacing:'0.06em',
textTransform:'uppercase',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{system.name}
{system.detail && (
<span style={{ color: 'var(--text-muted)', marginLeft: '0.5rem', textTransform: 'none' }}>
{system.detail}
</span>
)}
</span>
{/* Status dot + label */}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<span
style={{
display: 'inline-block',
width: '6px',
height: '6px',
borderRadius: '50%',
background: color,
boxShadow: glow,
animation: isScanning ? 'blink-cursor 1s step-end infinite' : undefined,
}}
/>
<span
style={{
fontSize: '0.6rem',
color,
letterSpacing: '0.08em',
}}
>
{system.status}
</span>
</div>
</div>
)
})}
</div>
</div>
)
}
)
StatusGrid.displayName = 'StatusGrid'
export { StatusGrid }
+72
View File
@@ -0,0 +1,72 @@
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from '@/lib/utils'
const Tabs = TabsPrimitive.Root
const TabsGroup = TabsPrimitive.List // alias
/* ── List ── */
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex items-end',
'border-b border-[var(--border)]',
'w-full gap-0',
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
/* ── Trigger ── */
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center',
'px-4 py-2',
'text-[0.7rem] font-mono font-medium uppercase tracking-widest',
'border-b-2 border-transparent',
'-mb-px',
'text-[var(--text-muted)]',
'cursor-pointer',
'outline-none',
'transition-all duration-150',
'hover:text-[var(--text-secondary)]',
'data-[state=active]:text-[var(--color-green)]',
'data-[state=active]:border-b-[var(--color-green)]',
'data-[state=active]:[text-shadow:var(--text-glow-green)]',
'disabled:pointer-events-none disabled:opacity-40',
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
/* ── Content ── */
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-4',
'focus-visible:outline-none',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsGroup, TabsList, TabsTrigger, TabsContent }
+187
View File
@@ -0,0 +1,187 @@
'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
export type TerminalLineType = 'input' | 'output' | 'warn' | 'error' | 'system'
export interface TerminalLine {
type?: TerminalLineType
text: string
timestamp?: string
}
export interface TerminalProps extends React.HTMLAttributes<HTMLDivElement> {
lines?: TerminalLine[]
prompt?: string
title?: string
height?: string | number
blinkCursor?: boolean
}
function lineColor(type: TerminalLineType = 'output'): string {
switch (type) {
case 'input': return 'var(--color-green)'
case 'output': return 'var(--text-secondary)'
case 'warn': return 'var(--color-amber)'
case 'error': return 'var(--color-red)'
case 'system': return 'var(--text-muted)'
}
}
function linePrefix(type: TerminalLineType = 'output'): string {
switch (type) {
case 'input': return '>'
case 'output': return ' '
case 'warn': return '⚠'
case 'error': return '✕'
case 'system': return '#'
}
}
const Terminal = React.forwardRef<HTMLDivElement, TerminalProps>(
(
{
className,
lines = [],
prompt = '>',
title = 'TERMINAL',
height = '16rem',
blinkCursor = true,
style,
...props
},
ref
) => {
const bodyRef = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
if (bodyRef.current) {
bodyRef.current.scrollTop = bodyRef.current.scrollHeight
}
}, [lines])
return (
<div
ref={ref}
className={cn(className)}
style={{
border: '1px solid var(--border)',
background: 'var(--surface)',
fontFamily: 'var(--font-mono)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
...style,
}}
{...props}
>
{/* Title bar */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.4rem 0.75rem',
borderBottom: '1px solid var(--border)',
background: 'var(--surface-raised)',
}}
>
<span
style={{
fontSize: '0.6rem',
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}
>
{title}
</span>
{/* Status dot */}
<span
style={{
marginLeft: 'auto',
width: '6px',
height: '6px',
borderRadius: '50%',
background: 'var(--color-green)',
boxShadow: 'var(--glow-green)',
animation: 'blink-cursor 2s step-end infinite',
}}
/>
</div>
{/* Output body */}
<div
ref={bodyRef}
style={{
flex: 1,
overflowY: 'auto',
padding: '0.75rem',
height,
display: 'flex',
flexDirection: 'column',
gap: '0.2rem',
}}
>
{lines.map((line, i) => (
<div
key={i}
style={{
display: 'flex',
gap: '0.5rem',
fontSize: '0.75rem',
lineHeight: 1.6,
color: lineColor(line.type),
}}
>
{line.timestamp && (
<span
style={{
color: 'var(--text-muted)',
flexShrink: 0,
fontSize: '0.65rem',
}}
>
{line.timestamp}
</span>
)}
<span style={{ flexShrink: 0, opacity: 0.5, userSelect: 'none' }}>
{linePrefix(line.type)}
</span>
<span style={{ wordBreak: 'break-all' }}>{line.text}</span>
</div>
))}
{/* Blinking cursor */}
{blinkCursor && (
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: '0.75rem',
marginTop: '0.1rem',
gap: '0.25rem',
}}
>
<span style={{ color: 'var(--text-muted)', userSelect: 'none' }}>{prompt}</span>
<span
style={{
display: 'inline-block',
width: '0.55rem',
height: '1em',
background: 'var(--color-green)',
boxShadow: 'var(--glow-green)',
animation: 'blink-cursor 1s step-end infinite',
}}
/>
</div>
)}
</div>
</div>
)
}
)
Terminal.displayName = 'Terminal'
export { Terminal }