Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@
}

:root {
--bg: #ffffff;
--bg-panel: rgba(240, 240, 240, 0.78);
--bg-panel-strong: #e5e5e5;
--text-strong: #000000;
--text: #1a1a1a;
--muted: #666666;
--line: rgba(0, 0, 0, 0.11);
--brand: #d8481f;
--brand-strong: #c2410c;
--brand-soft: #ffedd5;
--accent: #0f766e;
--success: #059669;
--warning: #d97706;
--body-bg: radial-gradient(circle at top left, rgba(240, 100, 45, 0.05), transparent 28%),
radial-gradient(circle at 80% 10%, rgba(37, 181, 164, 0.05), transparent 22%),
linear-gradient(180deg, #f8fafc 0%, #ffffff 45%, #f1f5f9 100%);
--background: var(--bg);
--foreground: var(--text);
}

.dark {
--bg: #09111d;
--bg-panel: rgba(14, 21, 32, 0.78);
--bg-panel-strong: #121d2b;
Expand All @@ -21,6 +42,28 @@
--accent: #25b5a4;
--success: #34c789;
--warning: #f3a63c;
--body-bg: radial-gradient(circle at top left, rgba(240, 100, 45, 0.18), transparent 28%),
radial-gradient(circle at 80% 10%, rgba(37, 181, 164, 0.12), transparent 22%),
linear-gradient(180deg, #07101a 0%, #09111d 45%, #0b1320 100%);
--background: var(--bg);
--foreground: var(--text);
}

.oled {
--bg: #000000;
--bg-panel: #000000;
--bg-panel-strong: #000000;
--text-strong: #ffffff;
--text: #ffffff;
--muted: #cccccc;
--line: #555555;
--brand: #ffaa00;
--brand-strong: #ffcc00;
--brand-soft: #ff8800;
--accent: #00ffff;
--success: #00ff00;
--warning: #ffcc00;
--body-bg: #000000;
--background: var(--bg);
--foreground: var(--text);
}
Expand Down Expand Up @@ -62,10 +105,7 @@ html {
body {
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, rgba(240, 100, 45, 0.18), transparent 28%),
radial-gradient(circle at 80% 10%, rgba(37, 181, 164, 0.12), transparent 22%),
linear-gradient(180deg, #07101a 0%, #09111d 45%, #0b1320 100%);
background: var(--body-bg);
color: var(--text);
font-family: var(--font-sans), sans-serif;
}
Expand Down
20 changes: 1 addition & 19 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { OfflineSyncHandler } from '@/components/OfflineSyncHandler';
import { SkipLink } from '@/components/ui/SkipLink';
import { AuthProvider } from '@/contexts/AuthContext';
import { NotificationProvider } from '@/contexts/NotificationContext';
import { ThemeProvider } from '@/contexts/ThemeContext';
import { Providers as ThemeProvider } from '@/lib/theme/providers';
import { TutorialProvider } from '@/contexts/TutorialContext';
import { WalletProvider } from '@/contexts/WalletContext';
import { Web3OnboardingProvider } from '@/contexts/Web3OnboardingContext';
Expand Down Expand Up @@ -43,24 +43,6 @@ export default function RootLayout({
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;700;900&display=swap"
rel="stylesheet"
/>
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `
(function() {
try {
var theme = localStorage.getItem('web3-lab-theme');
var isDark = theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch (e) {}
})();
`,
}}
/>
</head>
<body className="bg-background text-foreground min-h-screen antialiased" suppressHydrationWarning>
<ThemeProvider>
Expand Down
57 changes: 54 additions & 3 deletions frontend/src/components/playground/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { extendRustLanguage } from '@/lib/editor/SorobanLanguage';
import type { SorobanLinterInstance } from '@/lib/editor/SorobanLinter';
import { createSorobanLinter } from '@/lib/editor/SorobanLinter';
import { THEME_COLORS } from '@/lib/theme/themeColors';
import { useThemeMode } from '@/hooks/useThemeMode';
import type { OnMount } from '@monaco-editor/react';
import { ChevronRight, FileText } from 'lucide-react';
import type { editor } from 'monaco-editor';
Expand Down Expand Up @@ -127,6 +128,55 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
const linterRef = useRef<SorobanLinterInstance | null>(null);
const compileActionRef = useRef<{ dispose: () => void } | null>(null);
const prefersReducedMotion = useMemo(() => getPrefersReducedMotion(), []);

const { theme, colors } = useThemeMode();

useEffect(() => {
if (!editorInstance) return;
const monaco = (window as any).monaco;
if (!monaco) return;

const baseTheme = theme === 'light' ? 'vs' : 'hc-black';
monaco.editor.defineTheme('web3-lab-premium', {
base: baseTheme,
inherit: true,
rules: [
{ token: 'comment', foreground: '636e7b', fontStyle: 'italic' },
{ token: 'keyword', foreground: 'ff7b72', fontStyle: 'bold' },
{ token: 'string', foreground: 'a5d6ff' },
{ token: 'type', foreground: '79c0ff' },
{ token: 'function', foreground: 'd2a8ff' },
{
token: 'sorobanMacro',
foreground: colors.interactive.primary.replace('#', ''),
fontStyle: 'bold',
},
{
token: 'sorobanType',
foreground: colors.status.info.replace('#', ''),
fontStyle: 'bold',
},
{
token: 'sorobanModule',
foreground: colors.status.warning.replace('#', ''),
},
{
token: 'moduleSeparator',
foreground: colors.text.muted.replace('#', ''),
},
],
colors: {
'editor.background': colors.background.primary,
'editor.lineHighlightBackground': theme === 'light' ? '#00000005' : '#ffffff05',
'editorCursor.foreground': colors.status.error,
'editor.selectionBackground': `${colors.status.error}22`,
'editorLineNumber.foreground': colors.text.muted,
'editorLineNumber.activeForeground': colors.text.secondary,
},
});
monaco.editor.setTheme('web3-lab-premium');
}, [editorInstance, theme, colors]);


const collaboratorLabel = useMemo(() => {
if (collaborationProvider) {
Expand Down Expand Up @@ -168,9 +218,9 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
registerSorobanCompletion(monaco);
registerSorobanHover(monaco);

const colors = THEME_COLORS.dark;
const baseTheme = theme === 'light' ? 'vs' : 'hc-black';
monaco.editor.defineTheme('web3-lab-premium', {
base: 'vs-dark',
base: baseTheme,
inherit: true,
rules: [
{ token: 'comment', foreground: '636e7b', fontStyle: 'italic' },
Expand Down Expand Up @@ -199,7 +249,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
],
colors: {
'editor.background': colors.background.primary,
'editor.lineHighlightBackground': '#ffffff05',
'editor.lineHighlightBackground': theme === 'light' ? '#00000005' : '#ffffff05',
'editorCursor.foreground': colors.status.error,
'editor.selectionBackground': `${colors.status.error}22`,
'editorLineNumber.foreground': colors.text.muted,
Expand All @@ -208,6 +258,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
});
monaco.editor.setTheme('web3-lab-premium');


const model = mountedEditor.getModel();
if (model) {
model.updateOptions({ tabSize: settings.tabSize, insertSpaces: true });
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/theme/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ export const ThemeToggle: React.FC<ThemeToggleProps> = ({
whileTap={{ scale: 0.95 }}
onClick={toggleTheme}
className={`flex items-center gap-2 rounded-lg bg-gray-900 px-3 py-2 text-gray-400 transition-colors hover:bg-gray-800 hover:text-gray-300 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-black focus:outline-none dark:bg-gray-100 dark:text-gray-600 dark:hover:bg-gray-200 dark:hover:text-gray-700 dark:focus:ring-offset-white ${className} `}
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
aria-label={`Switch theme`}
>
<AnimatedThemeIcon isDark={isDark} size={size} />
{showLabel && <span className="text-sm font-medium">{isDark ? 'Dark' : 'Light'}</span>}
{showLabel && <span className="text-sm font-medium">{theme.charAt(0).toUpperCase() + theme.slice(1)}</span>}
</motion.button>
);
}
Expand All @@ -119,7 +119,7 @@ export const ThemeToggle: React.FC<ThemeToggleProps> = ({
whileTap={{ scale: 0.9 }}
onClick={toggleTheme}
className={`relative flex items-center justify-center rounded-lg ${sizeMap[size]} bg-gray-900 text-gray-400 transition-colors hover:bg-gray-800 hover:text-gray-300 focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 focus:ring-offset-black focus:outline-none dark:bg-gray-100 dark:text-gray-600 dark:hover:bg-gray-200 dark:hover:text-gray-700 dark:focus:ring-offset-white ${className} `}
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
aria-label={`Switch theme`}
type="button"
>
<AnimatedThemeIcon isDark={isDark} size={size} />
Expand Down Expand Up @@ -213,7 +213,7 @@ export const ThemeToggleCompact: React.FC<{ className?: string }> = ({ className
whileTap={{ scale: 0.9 }}
onClick={toggleTheme}
className={`rounded-md p-1.5 text-gray-400 transition-colors hover:bg-gray-900 hover:text-gray-300 focus:ring-2 focus:ring-purple-500 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white ${className} `}
aria-label={`Switch to ${isDark ? 'light' : 'dark'} mode`}
aria-label={`Switch theme`}
>
<motion.div
animate={{ rotate: isDark ? 180 : 0 }}
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/hooks/useThemeMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function useThemeMode() {
// If theme is 'system', use the detected systemTheme
// If not mounted yet (hydration in progress), default to 'dark'
const currentTheme = mounted ? (theme === 'system' ? systemTheme : theme) : 'dark';
const isDark = currentTheme === 'dark';
const isDark = currentTheme === 'dark' || currentTheme === 'oled';
const isLight = currentTheme === 'light';

/**
Expand All @@ -79,7 +79,10 @@ export function useThemeMode() {
* 5. Triggers smooth transition via CSS
*/
const toggleTheme = () => {
const newTheme = isDark ? 'light' : 'dark';
let newTheme: 'light' | 'dark' | 'oled' = 'dark';
if (currentTheme === 'light') newTheme = 'dark';
else if (currentTheme === 'dark') newTheme = 'oled';
else newTheme = 'light';
setTheme(newTheme);
setStoredTheme(newTheme);
};
Expand All @@ -94,20 +97,20 @@ export function useThemeMode() {
* - 'dark': Force dark mode
* - 'system': Use OS preference
*/
const setThemeMode = (newTheme: 'light' | 'dark' | 'system') => {
const setThemeMode = (newTheme: 'light' | 'dark' | 'oled' | 'system') => {
setTheme(newTheme);
setStoredTheme(newTheme);
};

// Get current theme colors
const colors = currentTheme === 'light' ? THEME_COLORS.light : THEME_COLORS.dark;
const colors = currentTheme === 'light' ? THEME_COLORS.light : currentTheme === 'oled' ? THEME_COLORS.oled : THEME_COLORS.dark;

// Get chart colors optimized for D3 visualizations
// These colors are chosen for good contrast and visual distinction
const chartColors = getChartColors(currentTheme as 'light' | 'dark');
const chartColors = getChartColors((currentTheme === 'light' ? 'light' : currentTheme === 'oled' ? 'oled' : 'dark') as any);

return {
theme: currentTheme as 'light' | 'dark',
theme: currentTheme as 'light' | 'dark' | 'oled',
isDark,
isLight,
mounted, // Use this to conditionally render to prevent FOUC
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/lib/theme/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,6 @@ import { ThemeProvider } from 'next-themes';
import { ReactNode, useEffect, useState } from 'react';

export function Providers({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false);

// Prevent FOUC by waiting for hydration
useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return <>{children}</>;
}

return (
<ThemeProvider
attribute="class"
Expand Down
49 changes: 46 additions & 3 deletions frontend/src/lib/theme/themeColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,49 @@ export const THEME_COLORS = {
text: '#ffffff',
},
},
oled: {
// Background colors
background: {
primary: '#000000',
secondary: '#000000',
tertiary: '#000000',
accent: '#000000',
},
// Text colors
text: {
primary: '#ffffff',
secondary: '#ffffff',
tertiary: '#ffffff',
muted: '#cccccc',
},
// Border colors
border: {
light: '#555555',
medium: '#666666',
dark: '#777777',
},
// Interactive colors
interactive: {
primary: '#ffaa00',
primaryHover: '#ffcc00',
secondary: '#333333',
secondaryHover: '#444444',
},
// Status colors
status: {
success: '#00ff00',
error: '#ff0000',
warning: '#ffcc00',
info: '#00ffff',
},
// Chart colors
chart: {
primary: '#ff00ff',
accent: '#00ffff',
neutral: '#888888',
text: '#ffffff',
},
},
};

// CSS variable names
Expand Down Expand Up @@ -128,17 +171,17 @@ export const CSS_VARIABLES = {
};

// Helper to apply theme colors
export function getThemeColors(theme: 'light' | 'dark') {
export function getThemeColors(theme: 'light' | 'dark' | 'oled') {
return THEME_COLORS[theme];
}

// Helper to get chart colors for the current theme
export function getChartColors(theme: 'light' | 'dark') {
export function getChartColors(theme: 'light' | 'dark' | 'oled') {
return THEME_COLORS[theme].chart;
}

// Generate inline styles for theme
export function getThemeStyles(theme: 'light' | 'dark') {
export function getThemeStyles(theme: 'light' | 'dark' | 'oled') {
const colors = THEME_COLORS[theme];
const vars: Record<string, string> = {};

Expand Down
Loading