diff --git a/src/app/globals.css b/src/app/globals.css index 99d36fb..62b4b4d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,10 +1,54 @@ @import "tailwindcss"; +/* Global CSS variables for theming */ :root { + /* Core colors */ --background: #ececec; --foreground: #222; --primary: #ff4a00; --page-max-width: 1300px; + /* Derived colors */ + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-primary: var(--primary); + --color-screen: #f3f3f3; + --color-black-10: rgba(0, 0, 0, 0.1); + --color-black-50: rgba(0, 0, 0, 0.5); + /* Lighting settings: color (R,G,B) and opacity */ + --lighting-color: 248,248,248; + --lighting-opacity: 1; + --lighting-scale: 1; +} + +/* Dark mode overrides */ +html.dark { + --background: #1e1e1e; + --foreground: #e0e0e0; + --color-screen: #2a2a2a; + --color-black-10: rgba(255, 255, 255, 0.1); + --color-black-50: rgba(255, 255, 255, 0.5); + /* Lighting settings override for dark mode */ + --lighting-color: 255,255,255; + --lighting-opacity: 0.1; + --lighting-scale: 1; +} +/* Utility class for panel/input backgrounds, uses theme variable */ +.bg-screen { + background-color: var(--color-screen) !important; + position: relative; +} +.bg-screen::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: calc(100% - 1px); + height: calc(100% - 1px); + /* Panel/input border highlight: intensity scaled by lighting slider */ + border-top: 1px solid rgba(var(--lighting-color), calc(var(--lighting-opacity) * var(--lighting-scale))); + border-left: 1px solid rgba(var(--lighting-color), calc(var(--lighting-opacity) * var(--lighting-scale))); + border-radius: var(--radius-md); + pointer-events: none; } @theme inline { @@ -40,9 +84,10 @@ body { color: var(--foreground); @variant sm { + /* Gradient lighting effect using theme variables */ background-image: linear-gradient( to bottom right, - #f8f8f8, + rgba(var(--lighting-color), var(--lighting-opacity)), var(--background) 20% ); background-repeat: no-repeat; @@ -53,3 +98,14 @@ svg { display: block; flex-shrink: 0; } +/* CodeMirror dark theme overrides */ +html.dark .cm-editor { + background-color: var(--color-screen) !important; +} +html.dark .cm-editor .cm-content { + color: var(--color-foreground) !important; +} +html.dark .cm-editor .cm-gutters { + background-color: var(--color-screen) !important; + color: var(--color-foreground) !important; +} diff --git a/src/components/ui/Button.module.css b/src/components/ui/Button.module.css index 7edbb14..084deaf 100644 --- a/src/components/ui/Button.module.css +++ b/src/components/ui/Button.module.css @@ -8,7 +8,7 @@ flex: 1; border-radius: var(--radius-md); padding: 12px; - background: #f4f4f4; + background: var(--color-screen); box-shadow: rgb(255, 255, 255) 1px 1px 1px 0px inset, rgba(0, 0, 0, 0.15) -1px -1px 1px 0px inset, rgba(0, 0, 0, 0.26) 0.444584px 0.444584px 0.628737px -1px, @@ -20,6 +20,32 @@ transition: box-shadow 0.3s ease; cursor: pointer; user-select: none; + position: relative; + /* Per-button variables for lighting: base brightness and edge opacity */ + --lighting-brightness: 1; + --lighting-edge: 1; + /* Filter brightness = mix(global brightness (1) and per-element computed brightness) */ + /* Blend distance-based brightness with global lighting scale */ + filter: brightness( + calc( + var(--lighting-brightness) * (1 - var(--lighting-scale)) + + 1 * var(--lighting-scale) + ) + ); + } + /* Top-left reflective highlight based on lighting */ + .Button::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: calc(100% - 1px); + height: calc(100% - 1px); + /* Border highlight opacity based on distance and global slider */ + border-top: 1px solid rgba(var(--lighting-color), calc(var(--lighting-edge) * var(--lighting-scale))); + border-left: 1px solid rgba(var(--lighting-color), calc(var(--lighting-edge) * var(--lighting-scale))); + border-radius: inherit; + pointer-events: none; } .Button[data-block] { @@ -64,8 +90,9 @@ /* Secondary */ .Button[data-color="secondary"] { - color: #fff; - background: #222; + /* Contrast uses inverted base colors */ + color: var(--color-background); + background: var(--color-foreground); box-shadow: inset 1px 1px 1px #ffffffb3, inset -1px -1px 1px #0000003b, 0.444584px 0.444584px 0.628737px -0.75px #00000042, 1.21072px 1.21072px 1.71222px -1.5px #0000003f, @@ -86,8 +113,9 @@ /* Tertiary */ .Button[data-color="tertiary"] { - color: #fff; - background: #6a6a6a; + /* Subtle accent using semi-transparent base */ + color: var(--color-background); + background: var(--color-black-50); box-shadow: inset 1px 1px 1px #ffffffba, inset -1px -1px 1px #0000003b, 0.444584px 0.444584px 0.628737px -1px #00000042, 1.21072px 1.21072px 1.71222px -1.5px #0000003f, @@ -108,8 +136,9 @@ /* Neutral */ .Button[data-color="neutral"] { - color: #fff; - background: #aaa; + /* Low-contrast neutral: use foreground for icon/text clarity */ + color: var(--color-foreground); + background: var(--color-black-10); box-shadow: inset 1px 1px 1px #ffffffc2, inset -1px -1px 1px #0000003b, 0.444584px 0.444584px 0.628737px -1px #00000042, 1.21072px 1.21072px 1.71222px -1.5px #0000003f, diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 70fb15a..a19d9d3 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,6 +1,6 @@ import { useAudioClip } from "@/hooks/useAudioClip"; import clsx from "clsx"; -import { KeyboardEvent, MouseEvent, ReactNode } from "react"; +import { KeyboardEvent, MouseEvent, ReactNode, useRef, useEffect } from "react"; import s from "./Button.module.css"; interface ButtonBaseProps { @@ -39,6 +39,36 @@ export const Button = ({ }: ButtonProps) => { const TagName = href ? "a" : "div"; const playPressed = useAudioClip("/pressed.wav"); + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) return; + + const updateLighting = () => { + const el = ref.current!; + const rect = el.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + const maxDist = Math.hypot(window.innerWidth, window.innerHeight); + const dist = Math.hypot(x, y); + const norm = dist / maxDist; + // base brightness factor (1 = full, minB = dimmest at furthest point) + const minB = 0.6; + const brightness = minB + (1 - norm) * (1 - minB); + // set base brightness (distance-based) and edge opacity for this button + el.style.setProperty("--lighting-brightness", brightness.toString()); + const baseEdge = Math.max(0, Math.min(1, 1 - norm)); + el.style.setProperty("--lighting-edge", baseEdge.toString()); + }; + + updateLighting(); + window.addEventListener("resize", updateLighting); + window.addEventListener("scroll", updateLighting, true); + return () => { + window.removeEventListener("resize", updateLighting); + window.removeEventListener("scroll", updateLighting, true); + }; + }, []); const handleClick = (evt: MouseEvent) => { if (!selected) { @@ -54,7 +84,7 @@ export const Button = ({ }; return ( - { const voice = appStore.useState((state) => state.voice); const input = appStore.useState((state) => state.input); const prompt = appStore.useState((state) => state.prompt); const height = "563px"; const codeView = appStore.useState((state) => state.codeView); + // Track dark mode to switch CodeMirror theme + const [isDark, setIsDark] = useState( + document.documentElement.classList.contains("dark") + ); + useEffect(() => { + const obs = new MutationObserver(() => { + setIsDark(document.documentElement.classList.contains("dark")); + }); + obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); + return () => obs.disconnect(); + }, []); const editorTheme = EditorView.theme({ // Only highlight the line if the editor is in a focused state. @@ -89,7 +134,7 @@ export const DevMode: React.FC = () => { height={height} extensions={[python(), editorTheme]} basicSetup={setup} - theme={fmTheme} + theme={isDark ? fmDarkTheme : fmTheme} />
@@ -98,7 +143,7 @@ export const DevMode: React.FC = () => { height={height} extensions={[javascript(), editorTheme]} basicSetup={setup} - theme={fmTheme} + theme={isDark ? fmDarkTheme : fmTheme} />
@@ -107,7 +152,7 @@ export const DevMode: React.FC = () => { height={height} extensions={[editorTheme]} basicSetup={setup} - theme={fmTheme} + theme={isDark ? fmDarkTheme : fmTheme} />
diff --git a/src/components/ui/Footer.module.css b/src/components/ui/Footer.module.css index 7ffbadc..8056b9f 100644 --- a/src/components/ui/Footer.module.css +++ b/src/components/ui/Footer.module.css @@ -5,7 +5,8 @@ left: 0; z-index: 20; border-top: 1px solid transparent; - background: #ececec70; + /* Solid background respects theme background variable */ + background: var(--background); backdrop-filter: blur(2rem); -webkit-backdrop-filter: blur(2rem); font-size: 1.125rem; @@ -13,7 +14,8 @@ } [data-scrollable="true"] .Footer { - border-top: 1px solid #fff; + /* Border adapts to theme contrast */ + border-top: 1px solid var(--color-black-50); } @keyframes wave { diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index c37e5a5..b87278b 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -2,6 +2,9 @@ import { useAudioClip } from "@/hooks/useAudioClip"; import { Switcher } from "./Switcher"; import clsx from "clsx"; import { External } from "./Icons"; +import { useState, useEffect } from "react"; +import { Switch } from "radix-ui"; +import sSwitcher from "./Switcher.module.css"; interface HeaderProps { devMode: boolean; @@ -10,6 +13,39 @@ interface HeaderProps { export const Header = ({ devMode, setDevMode }: HeaderProps) => { const playToggle = useAudioClip("/click.wav"); + // Theme state: 'light' or 'dark' + const [theme, setTheme] = useState<"light" | "dark">("light"); + // Initialize theme from localStorage or OS preference + useEffect(() => { + const stored = localStorage.getItem("theme"); + if (stored === "dark" || stored === "light") { + setTheme(stored); + } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + setTheme("dark"); + } + }, []); + // Lighting scale slider state (0-100) + const [lighting, setLighting] = useState(100); + useEffect(() => { + const rootStyles = getComputedStyle(document.documentElement); + const init = parseFloat(rootStyles.getPropertyValue('--lighting-scale')) || 1; + setLighting(init * 100); + }, []); + const handleLightingChange = (val: number) => { + const scale = val / 100; + document.documentElement.style.setProperty('--lighting-scale', scale.toString()); + setLighting(val); + }; + // Apply theme class to and persist choice + useEffect(() => { + const root = document.documentElement; + if (theme === "dark") { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } + localStorage.setItem("theme", theme); + }, [theme]); return (
@@ -51,12 +87,45 @@ export const Header = ({ devMode, setDevMode }: HeaderProps) => {
-
+
setDevMode(checked)} id="dev-mode" /> + {/* Theme toggle */} +
+ + setTheme(checked ? 'dark' : 'light')} + > + + +
+ {/* Lighting slider */} +
+ + handleLightingChange(Number(e.target.value))} + className="w-24 h-1 bg-screen rounded-lg appearance-none cursor-pointer" + style={{ accentColor: 'var(--primary)' }} + /> +
diff --git a/tailwind.config.mjs b/tailwind.config.mjs index ac1cbbb..68a4e26 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -1,4 +1,6 @@ const config = { + // Enable class-based dark mode + darkMode: 'class', content: [ "./src/**/*.{html,js,jsx,ts,tsx}", // adjust paths as needed "./public/**/*.html",