Skip to content
Draft
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
138 changes: 138 additions & 0 deletions devussy-web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,144 @@
--sidebar-ring: oklch(0.65 0.25 265);
}

.forest {
--radius: 0.5rem;
--background: oklch(0.98 0.01 120);
--foreground: oklch(0.15 0.02 120);
--card: oklch(1 0 0);
--card-foreground: oklch(0.15 0.02 120);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.15 0.02 120);
--primary: oklch(0.35 0.15 145);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.96 0.01 120);
--secondary-foreground: oklch(0.35 0.15 145);
--muted: oklch(0.96 0.01 120);
--muted-foreground: oklch(0.55 0.03 120);
--accent: oklch(0.96 0.01 120);
--accent-foreground: oklch(0.35 0.15 145);
--destructive: oklch(0.6 0.25 25);
--border: oklch(0.92 0.01 120);
--input: oklch(0.92 0.01 120);
--ring: oklch(0.35 0.15 145);
--chart-1: oklch(0.35 0.15 145);
--chart-2: oklch(0.45 0.2 170);
--chart-3: oklch(0.25 0.1 120);
--chart-4: oklch(0.55 0.15 190);
--chart-5: oklch(0.65 0.2 210);
--sidebar: oklch(0.98 0.01 120);
--sidebar-foreground: oklch(0.15 0.02 120);
--sidebar-primary: oklch(0.35 0.15 145);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.96 0.01 120);
--sidebar-accent-foreground: oklch(0.35 0.15 145);
--sidebar-border: oklch(0.92 0.01 120);
--sidebar-ring: oklch(0.35 0.15 145);
}

.dark.forest {
--background: oklch(0.2 0.05 140);
--foreground: oklch(0.98 0.02 140);
--card: oklch(0.15 0.06 140);
--card-foreground: oklch(0.98 0.02 140);
--popover: oklch(0.15 0.06 140);
--popover-foreground: oklch(0.98 0.02 140);
--primary: oklch(0.65 0.25 145);
--primary-foreground: oklch(0.10 0.05 140);
--secondary: oklch(0.25 0.10 140);
--secondary-foreground: oklch(0.98 0.02 140);
--muted: oklch(0.25 0.10 140);
--muted-foreground: oklch(0.70 0.05 140);
--accent: oklch(0.25 0.10 140);
--accent-foreground: oklch(0.98 0.02 140);
--destructive: oklch(0.6 0.25 25);
--border: oklch(0.30 0.10 140);
--input: oklch(0.30 0.10 140);
--ring: oklch(0.65 0.25 145);
--chart-1: oklch(0.65 0.25 145);
--chart-2: oklch(0.75 0.20 170);
--chart-3: oklch(0.55 0.25 120);
--chart-4: oklch(0.85 0.15 190);
--chart-5: oklch(0.90 0.10 210);
--sidebar: oklch(0.15 0.06 140);
--sidebar-foreground: oklch(0.98 0.02 140);
--sidebar-primary: oklch(0.65 0.25 145);
--sidebar-primary-foreground: oklch(0.10 0.05 140);
--sidebar-accent: oklch(0.25 0.10 140);
--sidebar-accent-foreground: oklch(0.98 0.02 140);
--sidebar-border: oklch(0.30 0.10 140);
--sidebar-ring: oklch(0.65 0.25 145);
}

.ocean {
--radius: 0.5rem;
--background: oklch(0.98 0.01 200);
--foreground: oklch(0.15 0.02 200);
--card: oklch(1 0 0);
--card-foreground: oklch(0.15 0.02 200);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.15 0.02 200);
--primary: oklch(0.35 0.15 225);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.96 0.01 200);
--secondary-foreground: oklch(0.35 0.15 225);
--muted: oklch(0.96 0.01 200);
--muted-foreground: oklch(0.55 0.03 200);
--accent: oklch(0.96 0.01 200);
--accent-foreground: oklch(0.35 0.15 225);
--destructive: oklch(0.6 0.25 25);
--border: oklch(0.92 0.01 200);
--input: oklch(0.92 0.01 200);
--ring: oklch(0.35 0.15 225);
--chart-1: oklch(0.35 0.15 225);
--chart-2: oklch(0.45 0.2 250);
--chart-3: oklch(0.25 0.1 200);
--chart-4: oklch(0.55 0.15 270);
--chart-5: oklch(0.65 0.2 290);
--sidebar: oklch(0.98 0.01 200);
--sidebar-foreground: oklch(0.15 0.02 200);
--sidebar-primary: oklch(0.35 0.15 225);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.96 0.01 200);
--sidebar-accent-foreground: oklch(0.35 0.15 225);
--sidebar-border: oklch(0.92 0.01 200);
--sidebar-ring: oklch(0.35 0.15 225);
}

.dark.ocean {
--background: oklch(0.2 0.05 220);
--foreground: oklch(0.98 0.02 220);
--card: oklch(0.15 0.06 220);
--card-foreground: oklch(0.98 0.02 220);
--popover: oklch(0.15 0.06 220);
--popover-foreground: oklch(0.98 0.02 220);
--primary: oklch(0.65 0.25 225);
--primary-foreground: oklch(0.10 0.05 220);
--secondary: oklch(0.25 0.10 220);
--secondary-foreground: oklch(0.98 0.02 220);
--muted: oklch(0.25 0.10 220);
--muted-foreground: oklch(0.70 0.05 220);
--accent: oklch(0.25 0.10 220);
--accent-foreground: oklch(0.98 0.02 220);
--destructive: oklch(0.6 0.25 25);
--border: oklch(0.30 0.10 220);
--input: oklch(0.30 0.10 220);
--ring: oklch(0.65 0.25 225);
--chart-1: oklch(0.65 0.25 225);
--chart-2: oklch(0.75 0.20 250);
--chart-3: oklch(0.55 0.25 200);
--chart-4: oklch(0.85 0.15 270);
--chart-5: oklch(0.90 0.10 290);
--sidebar: oklch(0.15 0.06 220);
--sidebar-foreground: oklch(0.98 0.02 220);
--sidebar-primary: oklch(0.65 0.25 225);
--sidebar-primary-foreground: oklch(0.10 0.05 220);
--sidebar-accent: oklch(0.25 0.10 220);
--sidebar-accent-foreground: oklch(0.98 0.02 220);
--sidebar-border: oklch(0.30 0.10 220);
--sidebar-ring: oklch(0.65 0.25 225);
}

@layer base {
* {
@apply border-border;
Expand Down
17 changes: 12 additions & 5 deletions devussy-web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { JetBrains_Mono } from "next/font/google";
import "./globals.css";
import { AppLayout } from "@/components/layout/AppLayout";
import { ThemeProvider } from "@/components/ThemeProvider";
import { ThemeScript } from "@/components/ThemeScript";

const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"] });

Expand All @@ -16,12 +18,17 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<html lang="en">
<head>
<ThemeScript />
</head>
<body className={`${jetbrainsMono.className} antialiased`}>
<div className="scanlines" />
<AppLayout>
{children}
</AppLayout>
<ThemeProvider>
<div className="scanlines" />
<AppLayout>
{children}
</AppLayout>
</ThemeProvider>
</body>
</html>
);
Expand Down
74 changes: 74 additions & 0 deletions devussy-web/src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";

import React, { createContext, useContext, useState, useEffect } from 'react';

const themes = ["light", "dark", "forest", "dark forest", "ocean", "dark ocean"] as const;
type Theme = typeof themes[number];

interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
}

interface ThemeProviderState {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
}

const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined);

export function ThemeProvider({
children,
defaultTheme = "dark",
storageKey = "devussy-web-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
try {
return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
} catch (e) {
console.error("Failed to access localStorage:", e);
return defaultTheme;
}
});

useEffect(() => {
const root = window.document.documentElement;
root.className = theme.split(' ').join(' ');
}, [theme]);

const toggleTheme = () => {
const currentIndex = themes.indexOf(theme);
const nextIndex = (currentIndex + 1) % themes.length;
setTheme(themes[nextIndex]);
};

const value = {
theme,
setTheme: (newTheme: Theme) => {
try {
localStorage.setItem(storageKey, newTheme);
} catch (e) {
console.error("Failed to set theme in localStorage:", e);
}
setTheme(newTheme);
},
toggleTheme,
};

return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}

export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
18 changes: 18 additions & 0 deletions devussy-web/src/components/ThemeScript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import React from 'react';

const script = `
(function() {
try {
const theme = localStorage.getItem('devussy-web-theme') || 'dark';
document.documentElement.className = theme.split(' ').join(' ');
} catch (e) {
console.error('Failed to apply theme from localStorage', e);
}
})();
`;

export function ThemeScript() {
return <script dangerouslySetInnerHTML={{ __html: script }} />;
}
27 changes: 26 additions & 1 deletion devussy-web/src/components/window/Taskbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import React from 'react';
import { cn } from "@/utils";
import { Layout, HelpCircle, Plus } from "lucide-react";
import { Layout, HelpCircle, Plus, Sun, Moon, TreePine, Waves } from "lucide-react";
import { useTheme } from '../ThemeProvider';

interface TaskbarProps {
windows: Array<{ id: string; title: string; type: string }>;
Expand All @@ -21,12 +22,36 @@ export const Taskbar: React.FC<TaskbarProps> = ({
onNewProject,
onHelp
}) => {
const { theme, toggleTheme } = useTheme();

const ThemeIcon = () => {
if (theme.includes('forest')) {
return <TreePine className="h-4 w-4" />;
}
if (theme.includes('ocean')) {
return <Waves className="h-4 w-4" />;
}
if (theme.includes('dark')) {
return <Moon className="h-4 w-4" />;
}
return <Sun className="h-4 w-4" />;
};

return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-[100] flex items-center gap-2 p-2 rounded-xl bg-black/60 backdrop-blur-md border border-white/10 shadow-2xl transition-all hover:bg-black/70">
<div className="px-2 border-r border-white/10 mr-1">
<Layout className="h-5 w-5 text-primary" />
</div>

{/* Theme Toggle Button */}
<button
onClick={toggleTheme}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-muted-foreground hover:bg-white/5 hover:text-white"
title="Toggle Theme"
>
<ThemeIcon />
</button>

{/* New Project Button */}
<button
onClick={onNewProject}
Expand Down