Skip to content

Commit 4db69d2

Browse files
chriscrosstalkclaude
authored andcommitted
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>
1 parent ed0b0f7 commit 4db69d2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+503
-306
lines changed

admin/constants/kv_store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { KVStoreKey } from "../types/kv_store.js";
22

3-
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'system.earlyAccess', 'ai.assistantCustomName'];
3+
export const SETTINGS_KEYS: KVStoreKey[] = ['chat.suggestionsEnabled', 'chat.lastModel', 'ui.hasVisitedEasySetup', 'ui.theme', 'system.earlyAccess', 'ai.assistantCustomName'];

admin/inertia/app/app.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { generateUUID } from '~/lib/util'
1111
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
1212
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
1313
import NotificationsProvider from '~/providers/NotificationProvider'
14+
import { ThemeProvider } from '~/providers/ThemeProvider'
1415
import { UsePageProps } from '../../types/system'
1516

1617
const appName = import.meta.env.VITE_APP_NAME || 'Project N.O.M.A.D.'
@@ -38,14 +39,16 @@ createInertiaApp({
3839
const showDevtools = ['development', 'staging'].includes(environment)
3940
createRoot(el).render(
4041
<QueryClientProvider client={queryClient}>
41-
<TransmitProvider baseUrl={window.location.origin} enableLogging={true}>
42-
<NotificationsProvider>
43-
<ModalsProvider>
44-
<App {...props} />
45-
{showDevtools && <ReactQueryDevtools initialIsOpen={false} buttonPosition='bottom-left' />}
46-
</ModalsProvider>
47-
</NotificationsProvider>
48-
</TransmitProvider>
42+
<ThemeProvider>
43+
<TransmitProvider baseUrl={window.location.origin} enableLogging={true}>
44+
<NotificationsProvider>
45+
<ModalsProvider>
46+
<App {...props} />
47+
{showDevtools && <ReactQueryDevtools initialIsOpen={false} buttonPosition='bottom-left' />}
48+
</ModalsProvider>
49+
</NotificationsProvider>
50+
</TransmitProvider>
51+
</ThemeProvider>
4952
</QueryClientProvider>
5053
)
5154
},

admin/inertia/components/ActiveDownloads.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps)
3232
</div>
3333
))
3434
) : (
35-
<p className="text-gray-500">No active downloads</p>
35+
<p className="text-text-muted">No active downloads</p>
3636
)}
3737
</div>
3838
</>

admin/inertia/components/ActiveEmbedJobs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const ActiveEmbedJobs = ({ withHeader = false }: ActiveEmbedJobsProps) => {
3535
</div>
3636
))
3737
) : (
38-
<p className="text-gray-500">No files are currently being processed</p>
38+
<p className="text-text-muted">No files are currently being processed</p>
3939
)}
4040
</div>
4141
</>

admin/inertia/components/ActiveModelDownloads.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const ActiveModelDownloads = ({ withHeader = false }: ActiveModelDownloadsProps)
3333
</div>
3434
))
3535
) : (
36-
<p className="text-gray-500">No active model downloads</p>
36+
<p className="text-text-muted">No active model downloads</p>
3737
)}
3838
</div>
3939
</>

admin/inertia/components/Alert.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default function Alert({
4343
}
4444

4545
const getIconColor = () => {
46-
if (variant === 'solid') return 'text-desert-white'
46+
if (variant === 'solid') return 'text-white'
4747
switch (type) {
4848
case 'warning':
4949
return 'text-desert-orange'
@@ -81,15 +81,15 @@ export default function Alert({
8181
case 'solid':
8282
variantStyles.push(
8383
type === 'warning'
84-
? 'bg-desert-orange text-desert-white border border-desert-orange-dark'
84+
? 'bg-desert-orange text-white border border-desert-orange-dark'
8585
: type === 'error'
86-
? 'bg-desert-red text-desert-white border border-desert-red-dark'
86+
? 'bg-desert-red text-white border border-desert-red-dark'
8787
: type === 'success'
88-
? 'bg-desert-olive text-desert-white border border-desert-olive-dark'
88+
? 'bg-desert-olive text-white border border-desert-olive-dark'
8989
: type === 'info'
90-
? 'bg-desert-green text-desert-white border border-desert-green-dark'
90+
? 'bg-desert-green text-white border border-desert-green-dark'
9191
: type === 'info-inverted'
92-
? 'bg-desert-tan text-desert-white border border-desert-tan-dark'
92+
? 'bg-desert-tan text-white border border-desert-tan-dark'
9393
: ''
9494
)
9595
return classNames(baseStyles, 'shadow-lg', ...variantStyles)
@@ -112,7 +112,7 @@ export default function Alert({
112112
}
113113

114114
const getTitleColor = () => {
115-
if (variant === 'solid') return 'text-desert-white'
115+
if (variant === 'solid') return 'text-white'
116116

117117
switch (type) {
118118
case 'warning':
@@ -131,7 +131,7 @@ export default function Alert({
131131
}
132132

133133
const getMessageColor = () => {
134-
if (variant === 'solid') return 'text-desert-white text-opacity-90'
134+
if (variant === 'solid') return 'text-white text-opacity-90'
135135

136136
switch (type) {
137137
case 'warning':
@@ -149,7 +149,7 @@ export default function Alert({
149149

150150
const getCloseButtonStyles = () => {
151151
if (variant === 'solid') {
152-
return 'text-desert-white hover:text-desert-white hover:bg-black hover:bg-opacity-20'
152+
return 'text-white hover:text-white hover:bg-black hover:bg-opacity-20'
153153
}
154154

155155
switch (type) {

admin/inertia/components/BouncingDots.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ interface BouncingDotsProps {
99
export default function BouncingDots({ text, containerClassName, textClassName }: BouncingDotsProps) {
1010
return (
1111
<div className={clsx("flex items-center justify-center gap-2", containerClassName)}>
12-
<span className={clsx("text-gray-600", textClassName)}>{text}</span>
12+
<span className={clsx("text-text-secondary", textClassName)}>{text}</span>
1313
<span className="flex gap-1 mt-1">
1414
<span
15-
className="w-1.5 h-1.5 bg-gray-600 rounded-full animate-bounce"
15+
className="w-1.5 h-1.5 bg-text-secondary rounded-full animate-bounce"
1616
style={{ animationDelay: '0ms' }}
1717
/>
1818
<span
19-
className="w-1.5 h-1.5 bg-gray-600 rounded-full animate-bounce"
19+
className="w-1.5 h-1.5 bg-text-secondary rounded-full animate-bounce"
2020
style={{ animationDelay: '150ms' }}
2121
/>
2222
<span
23-
className="w-1.5 h-1.5 bg-gray-600 rounded-full animate-bounce"
23+
className="w-1.5 h-1.5 bg-text-secondary rounded-full animate-bounce"
2424
style={{ animationDelay: '300ms' }}
2525
/>
2626
</span>

admin/inertia/components/DownloadURLModal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const DownloadURLModal: React.FC<DownloadURLModalProps> = ({
6363
large
6464
>
6565
<div className="flex flex-col pb-4">
66-
<p className="text-gray-700 mb-8">
66+
<p className="text-text-secondary mb-8">
6767
Enter the URL of the map region file you wish to download. The URL must be publicly
6868
reachable and end with .pmtiles. A preflight check will be run to verify the file's
6969
availability, type, and approximate size.
@@ -76,11 +76,11 @@ const DownloadURLModal: React.FC<DownloadURLModalProps> = ({
7676
value={url}
7777
onChange={(e) => setUrl(e.target.value)}
7878
/>
79-
<div className="min-h-24 max-h-96 overflow-y-auto bg-gray-50 p-4 rounded border border-gray-300 text-left">
79+
<div className="min-h-24 max-h-96 overflow-y-auto bg-surface-secondary p-4 rounded border border-border-default text-left">
8080
{messages.map((message, idx) => (
8181
<p
8282
key={idx}
83-
className="text-sm text-gray-900 font-mono leading-relaxed break-words mb-3"
83+
className="text-sm text-text-primary font-mono leading-relaxed break-words mb-3"
8484
>
8585
{message}
8686
</p>

admin/inertia/components/Footer.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { usePage } from '@inertiajs/react'
22
import { UsePageProps } from '../../types/system'
3+
import ThemeToggle from '~/components/ThemeToggle'
34

45
export default function Footer() {
56
const { appVersion } = usePage().props as unknown as UsePageProps
67
return (
7-
<footer className="">
8-
<div className="flex justify-center border-t border-gray-900/10 py-4">
9-
<p className="text-sm/6 text-gray-600">
8+
<footer>
9+
<div className="flex items-center justify-center gap-3 border-t border-border-subtle py-4">
10+
<p className="text-sm/6 text-text-secondary">
1011
Project N.O.M.A.D. Command Center v{appVersion}
1112
</p>
13+
<ThemeToggle />
1214
</div>
1315
</footer>
1416
)

admin/inertia/components/HorizontalBarChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export default function HorizontalBarChart({
9494
className={classNames(
9595
'absolute top-1/2 -translate-y-1/2 font-bold text-sm',
9696
item.value > 15
97-
? 'left-3 text-desert-white drop-shadow-md'
97+
? 'left-3 text-white drop-shadow-md'
9898
: 'right-3 text-desert-green'
9999
)}
100100
>

0 commit comments

Comments
 (0)