mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-19 00:03:33 +00:00
Merge d0772b8074 into cbe5dac8b7
This commit is contained in:
commit
31ddf20ee2
6 changed files with 177 additions and 9 deletions
|
|
@ -10,6 +10,25 @@
|
|||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
// Apply persisted theme before paint to avoid FOUC.
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('gitnexus.theme');
|
||||
var preference =
|
||||
stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
|
||||
var resolved =
|
||||
preference === 'system'
|
||||
? window.matchMedia('(prefers-color-scheme: light)').matches
|
||||
? 'light'
|
||||
: 'dark'
|
||||
: preference;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
} catch (e) {
|
||||
document.documentElement.dataset.theme = 'dark';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export const RightPanel = () => {
|
|||
<div className="flex items-center gap-2.5 border-b border-border-subtle bg-elevated/50 px-4 py-3">
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{!isAgentReady && (
|
||||
<span className="rounded-full border border-amber-500/30 bg-amber-500/15 px-2 py-1 text-[11px] text-amber-300">
|
||||
<span className="rounded-full border border-amber-500/30 bg-amber-500/15 px-2 py-1 text-[11px] text-amber-700 dark:text-amber-300">
|
||||
Configure AI
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -283,7 +283,7 @@ export const RightPanel = () => {
|
|||
|
||||
{/* Status / errors */}
|
||||
{agentError && (
|
||||
<div className="flex items-center gap-2 border-b border-rose-500/30 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">
|
||||
<div className="flex items-center gap-2 border-b border-rose-500/30 bg-rose-500/10 px-4 py-3 text-sm text-rose-700 dark:text-rose-100">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<span>{agentError}</span>
|
||||
</div>
|
||||
|
|
@ -445,7 +445,7 @@ export const RightPanel = () => {
|
|||
)}
|
||||
</div>
|
||||
{!isAgentReady && !isAgentInitializing && (
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-amber-200">
|
||||
<div className="mt-2 flex items-center gap-2 text-xs text-amber-700 dark:text-amber-200">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
<span>
|
||||
{isProviderConfigured()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
import type { LLMSettings, LLMProvider } from '../core/llm/types';
|
||||
import { DEFAULT_OLLAMA_BASE_URL } from '../config/ui-constants';
|
||||
import { ProviderConfigCard } from './settings/ProviderConfigCard';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
|
||||
interface SettingsPanelProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -368,6 +369,12 @@ export const SettingsPanel = ({
|
|||
|
||||
{/* Content */}
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-6">
|
||||
{/* Appearance */}
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-medium text-text-secondary">Appearance</label>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Local Server */}
|
||||
{backendUrl !== undefined && onBackendUrlChange && (
|
||||
<div className="space-y-3">
|
||||
|
|
@ -437,7 +444,7 @@ export const SettingsPanel = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-200">
|
||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-800 dark:text-amber-200">
|
||||
API keys are stored in session storage and will be cleared when you close this tab.
|
||||
</div>
|
||||
|
||||
|
|
|
|||
41
gitnexus-web/src/components/ThemeToggle.tsx
Normal file
41
gitnexus-web/src/components/ThemeToggle.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Monitor, Moon, Sun } from 'lucide-react';
|
||||
import { useTheme, type ThemePreference } from '../hooks/useTheme';
|
||||
|
||||
const OPTIONS: Array<{ value: ThemePreference; label: string; Icon: typeof Sun }> = [
|
||||
{ value: 'system', label: 'System', Icon: Monitor },
|
||||
{ value: 'light', label: 'Light', Icon: Sun },
|
||||
{ value: 'dark', label: 'Dark', Icon: Moon },
|
||||
];
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
const { preference, setPreference } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Theme"
|
||||
className="grid grid-cols-3 gap-2 rounded-xl border border-border-subtle bg-elevated p-1"
|
||||
>
|
||||
{OPTIONS.map(({ value, label, Icon }) => {
|
||||
const active = preference === value;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
onClick={() => setPreference(value)}
|
||||
className={`flex items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||
active
|
||||
? 'bg-accent/15 text-text-primary ring-1 ring-accent/40'
|
||||
: 'text-text-secondary hover:bg-hover hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
65
gitnexus-web/src/hooks/useTheme.ts
Normal file
65
gitnexus-web/src/hooks/useTheme.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export type ThemePreference = 'system' | 'dark' | 'light';
|
||||
export type ResolvedTheme = 'dark' | 'light';
|
||||
|
||||
const STORAGE_KEY = 'gitnexus.theme';
|
||||
const LIGHT_QUERY = '(prefers-color-scheme: light)';
|
||||
|
||||
const isPreference = (v: unknown): v is ThemePreference =>
|
||||
v === 'system' || v === 'dark' || v === 'light';
|
||||
|
||||
const readStoredPreference = (): ThemePreference => {
|
||||
try {
|
||||
const v = localStorage.getItem(STORAGE_KEY);
|
||||
return isPreference(v) ? v : 'system';
|
||||
} catch {
|
||||
return 'system';
|
||||
}
|
||||
};
|
||||
|
||||
const resolve = (pref: ThemePreference): ResolvedTheme => {
|
||||
if (pref === 'system') {
|
||||
return window.matchMedia(LIGHT_QUERY).matches ? 'light' : 'dark';
|
||||
}
|
||||
return pref;
|
||||
};
|
||||
|
||||
const apply = (resolved: ResolvedTheme) => {
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const [preference, setPreferenceState] = useState<ThemePreference>(readStoredPreference);
|
||||
const [resolved, setResolved] = useState<ResolvedTheme>(() => resolve(readStoredPreference()));
|
||||
|
||||
useEffect(() => {
|
||||
const next = resolve(preference);
|
||||
setResolved(next);
|
||||
apply(next);
|
||||
}, [preference]);
|
||||
|
||||
// When tracking the OS, follow live changes to the system preference.
|
||||
useEffect(() => {
|
||||
if (preference !== 'system') return;
|
||||
const mq = window.matchMedia(LIGHT_QUERY);
|
||||
const onChange = () => {
|
||||
const next: ResolvedTheme = mq.matches ? 'light' : 'dark';
|
||||
setResolved(next);
|
||||
apply(next);
|
||||
};
|
||||
mq.addEventListener('change', onChange);
|
||||
return () => mq.removeEventListener('change', onChange);
|
||||
}, [preference]);
|
||||
|
||||
const setPreference = useCallback((p: ThemePreference) => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, p);
|
||||
} catch {
|
||||
// localStorage may be unavailable (private mode); fall through.
|
||||
}
|
||||
setPreferenceState(p);
|
||||
}, []);
|
||||
|
||||
return { preference, resolved, setPreference };
|
||||
};
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
/* `dark:` modifier follows our explicit theme attribute, not the OS
|
||||
media query. The boot script and `useTheme` hook always write a
|
||||
resolved value (`light` | `dark`) to `<html data-theme>`, so this
|
||||
keeps the variant consistent with the runtime preference. */
|
||||
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
TAILWIND V4 THEME CONFIGURATION
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
|
@ -19,6 +25,8 @@
|
|||
--color-text-primary: #e4e4ed;
|
||||
--color-text-secondary: #8888a0;
|
||||
--color-text-muted: #5a5a70;
|
||||
/* Higher-contrast text used for emphasis (h1–h4, strong) */
|
||||
--color-text-strong: #ffffff;
|
||||
|
||||
/* Accent */
|
||||
--color-accent: #7c3aed;
|
||||
|
|
@ -49,16 +57,44 @@
|
|||
--shadow-glow-soft: 0 0 40px rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════
|
||||
LIGHT THEME — overrides applied when html[data-theme='light'].
|
||||
`@theme` above defines defaults (dark); these win by cascade.
|
||||
Only chrome tokens flip; node colors and accent stay constant
|
||||
so graph and brand identity are preserved across themes.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
[data-theme='light'] {
|
||||
--color-void: #ffffff;
|
||||
--color-deep: #f8f9fb;
|
||||
--color-surface: #ffffff;
|
||||
--color-elevated: #f3f4f7;
|
||||
--color-hover: #e9ebf0;
|
||||
|
||||
--color-border-subtle: #e5e7eb;
|
||||
--color-border-default: #d1d5db;
|
||||
|
||||
--color-text-primary: #1a1a23;
|
||||
--color-text-secondary: #525266;
|
||||
--color-text-muted: #80809a;
|
||||
--color-text-strong: #0a0a14;
|
||||
|
||||
--color-accent: #7c3aed;
|
||||
--color-accent-dim: #a78bfa;
|
||||
|
||||
--shadow-glow: 0 0 20px rgba(124, 58, 237, 0.18);
|
||||
--shadow-glow-soft: 0 0 40px rgba(124, 58, 237, 0.1);
|
||||
}
|
||||
|
||||
/* Keyframes */
|
||||
@keyframes breathe {
|
||||
0%,
|
||||
100% {
|
||||
border-color: #2a2a3a;
|
||||
border-color: var(--color-border-default);
|
||||
box-shadow: 0 0 0 0 rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-color: #7c3aed;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 40px 10px rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
}
|
||||
|
|
@ -149,7 +185,7 @@ body {
|
|||
═══════════════════════════════════════════════════════════════ */
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #2a2a3a #0a0a10;
|
||||
scrollbar-color: var(--color-border-default) var(--color-deep);
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
|
|
@ -200,7 +236,7 @@ body {
|
|||
|
||||
.chat-prose strong {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
color: var(--color-text-strong);
|
||||
}
|
||||
|
||||
.chat-prose em {
|
||||
|
|
@ -214,7 +250,7 @@ body {
|
|||
.chat-prose h3,
|
||||
.chat-prose h4 {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
color: var(--color-text-strong);
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue