project-nomad/admin/inertia/components/systeminfo/InfoCard.tsx
Chris Sherwood e8d775dfe4 feat(UI): add Night Ops dark mode with theme toggle
Add a warm charcoal dark mode ("Night Ops") using CSS variable swapping
under [data-theme="dark"]. All 23 desert palette variables are overridden
with dark-mode counterparts, and ~313 generic Tailwind classes (bg-white,
text-gray-*, border-gray-*) are replaced with semantic tokens.

Infrastructure:
- CSS variable overrides in app.css for both themes
- ThemeProvider + useTheme hook (localStorage + KV store sync)
- ThemeToggle component (moon/sun icons, "Night Ops"/"Day Ops" labels)
- FOUC prevention script in inertia_layout.edge
- Toggle placed in StyledSidebar and Footer for access on every page

Color replacements across 50 files:
- bg-white → bg-surface-primary
- bg-gray-50/100 → bg-surface-secondary
- text-gray-900/800 → text-text-primary
- text-gray-600/500 → text-text-secondary/text-text-muted
- border-gray-200/300 → border-border-subtle/border-border-default
- text-desert-white → text-white (fixes invisible text on colored bg)
- Button hover/active states use dedicated btn-green-hover/active vars

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:17:05 -07:00

78 lines
2.5 KiB
TypeScript

import classNames from '~/lib/classNames'
interface InfoCardProps {
title: string
icon?: React.ReactNode
data: Array<{
label: string
value: string | number | undefined
}>
variant?: 'default' | 'bordered' | 'elevated'
}
export default function InfoCard({ title, icon, data, variant = 'default' }: InfoCardProps) {
const getVariantStyles = () => {
switch (variant) {
case 'bordered':
return 'border-2 border-desert-green bg-desert-white'
case 'elevated':
return 'bg-desert-white shadow-lg border border-desert-stone-lighter'
default:
return 'bg-desert-white border border-desert-stone-light'
}
}
return (
<div
className={classNames(
'rounded-lg overflow-hidden transition-all duration-200 hover:shadow-xl',
getVariantStyles()
)}
>
<div className="relative bg-desert-green px-6 py-4 overflow-hidden">
{/* Diagonal line pattern */}
<div
className="absolute inset-0 opacity-10"
style={{
backgroundImage: `repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(255, 255, 255, 0.1) 10px,
rgba(255, 255, 255, 0.1) 20px
)`,
}}
/>
<div className="relative flex items-center gap-3">
{icon && <div className="text-white opacity-80">{icon}</div>}
<h3 className="text-lg font-bold text-white uppercase tracking-wide">{title}</h3>
</div>
<div className="absolute top-0 right-0 w-24 h-24 transform translate-x-8 -translate-y-8">
<div className="w-full h-full bg-desert-green-dark opacity-30 transform rotate-45" />
</div>
</div>
<div className="p-6">
<dl className="grid grid-cols-1 gap-4">
{data.map((item, index) => (
<div
key={index}
className={classNames(
'flex justify-between items-center py-2 border-b border-desert-stone-lighter last:border-b-0'
)}
>
<dt className="text-sm font-medium text-desert-stone-dark flex items-center gap-2">
{item.label}
</dt>
<dd className={classNames('text-sm font-semibold text-right text-desert-green-dark')}>
{item.value || 'N/A'}
</dd>
</div>
))}
</dl>
</div>
<div className="h-1 bg-desert-green" />
</div>
)
}