mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 19:49:25 +01:00
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>
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
import { IconCircleCheck, IconCircleX } from '@tabler/icons-react'
|
|
import classNames from '~/lib/classNames'
|
|
|
|
export type InstallActivityFeedProps = {
|
|
activity: Array<{
|
|
service_name: string
|
|
type:
|
|
| 'initializing'
|
|
| 'pulling'
|
|
| 'pulled'
|
|
| 'creating'
|
|
| 'created'
|
|
| 'preinstall'
|
|
| 'preinstall-complete'
|
|
| 'starting'
|
|
| 'started'
|
|
| 'finalizing'
|
|
| 'completed'
|
|
| 'update-pulling'
|
|
| 'update-stopping'
|
|
| 'update-creating'
|
|
| 'update-starting'
|
|
| 'update-complete'
|
|
| 'update-rollback'
|
|
timestamp: string
|
|
message: string
|
|
}>
|
|
className?: string
|
|
withHeader?: boolean
|
|
}
|
|
|
|
const InstallActivityFeed: React.FC<InstallActivityFeedProps> = ({ activity, className, withHeader = false }) => {
|
|
return (
|
|
<div className={classNames('bg-surface-primary shadow-sm rounded-lg p-6', className)}>
|
|
{withHeader && <h2 className="text-lg font-semibold text-text-primary">Installation Activity</h2>}
|
|
<ul role="list" className={classNames("space-y-6 text-desert-green", withHeader ? 'mt-6' : '')}>
|
|
{activity.map((activityItem, activityItemIdx) => (
|
|
<li key={activityItem.timestamp} className="relative flex gap-x-4">
|
|
<div
|
|
className={classNames(
|
|
activityItemIdx === activity.length - 1 ? 'h-6' : '-bottom-6',
|
|
'absolute left-0 top-0 flex w-6 justify-center'
|
|
)}
|
|
>
|
|
<div className="w-px bg-border-subtle" />
|
|
</div>
|
|
<>
|
|
<div className="relative flex size-6 flex-none items-center justify-center bg-transparent">
|
|
{activityItem.type === 'completed' || activityItem.type === 'update-complete' ? (
|
|
<IconCircleCheck aria-hidden="true" className="size-6 text-indigo-600" />
|
|
) : activityItem.type === 'update-rollback' ? (
|
|
<IconCircleX aria-hidden="true" className="size-6 text-red-500" />
|
|
) : (
|
|
<div className="size-1.5 rounded-full bg-surface-secondary ring-1 ring-border-default" />
|
|
)}
|
|
</div>
|
|
<p className="flex-auto py-0.5 text-xs/5 text-text-muted">
|
|
<span className="font-semibold text-text-primary">{activityItem.service_name}</span> -{' '}
|
|
{activityItem.type.charAt(0).toUpperCase() + activityItem.type.slice(1)}
|
|
</p>
|
|
<time
|
|
dateTime={activityItem.timestamp}
|
|
className="flex-none py-0.5 text-xs/5 text-text-muted"
|
|
>
|
|
{activityItem.timestamp}
|
|
</time>
|
|
</>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default InstallActivityFeed
|