From ee05c3f5d6d8dd746c06194d913661606bf324e8 Mon Sep 17 00:00:00 2001 From: dtoro Date: Fri, 6 Mar 2026 23:32:37 +0100 Subject: [PATCH] add theme --- src/App.tsx | 31 +++++++--- src/components/graph/ConfigNode.tsx | 6 +- src/components/graph/FunctionNode.tsx | 3 + src/components/graph/NodeHandles.tsx | 6 +- src/components/graph/RenderingNode.tsx | 2 +- src/lib/themeContext.tsx | 83 ++++++++++++++++++++++++++ src/main.tsx | 7 ++- src/styles.css | 6 +- 8 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 src/lib/themeContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 6ef2695..0d52eb2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,23 @@ import React from 'react' -import ReactFlow, { +import { + ReactFlow, Controls, Background, MiniMap, addEdge, - applyEdgeChanges, - applyNodeChanges, useNodesState, useEdgesState, - Node, - Edge, - Connection, - NodeChange, - EdgeChange, -} from 'reactflow' + type Node, + type Edge, + type Connection, + type ColorMode, +} from '@xyflow/react' import ConfigNode from './components/graph/ConfigNode' import FunctionNode from './components/graph/FunctionNode' import RenderingNode from './components/graph/RenderingNode' import VariableNode from './components/graph/VariableNode' import FlowContext from './lib/flowContext' +import { useTheme } from './lib/themeContext' import { ContextMenu, ContextMenuContent, @@ -27,6 +26,8 @@ import { ContextMenuLabel, ContextMenuTrigger, } from "@/components/ui/context-menu" +import { Button } from '@/components/ui/button' +import { Moon, Sun } from 'lucide-react' // Generate short unique node ids when missing const genId = () => `node_${Math.random().toString(36).slice(2, 9)}` @@ -48,6 +49,7 @@ const initialNodes: Node[] = [ const initialEdges: Edge[] = [{ id: 'e-config-render', source: 'config-1', target: 'render-1' }] export default function App() { + const { theme, toggleTheme } = useTheme() const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) const [rfInstance, setRfInstance] = React.useState(null) @@ -140,6 +142,16 @@ export default function App() { return (
+ @@ -151,6 +163,7 @@ export default function App() { onEdgesChange={onEdgesChange} onConnect={onConnect} nodeTypes={nodeTypes} + colorMode={theme as ColorMode} fitView onInit={onInit} > diff --git a/src/components/graph/ConfigNode.tsx b/src/components/graph/ConfigNode.tsx index f37112a..deff299 100644 --- a/src/components/graph/ConfigNode.tsx +++ b/src/components/graph/ConfigNode.tsx @@ -2,6 +2,7 @@ import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useSt import CodeMirror from '@uiw/react-codemirror' import { yaml } from '@codemirror/lang-yaml' import FlowContext from '../../lib/flowContext' +import { useTheme } from '../../lib/themeContext' import { BaseNode, BaseNodeContent, @@ -29,7 +30,7 @@ type Props = { export const ConfigNode = memo(function ConfigNode({ id, data }: Props) { const [value, setValue] = useState(data?.yaml ?? '# Enter YAML here\n') - + const { theme } = useTheme() const ctx = useContext(FlowContext) const setNodes = ctx?.setNodes const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? '' @@ -199,6 +200,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) { ref={editorRef} value={value} height="180px" + theme={theme} extensions={extensions} onChange={onChange} basicSetup={{ lineNumbers: true, foldGutter: false }} @@ -208,7 +210,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) { -
{storedYaml ? `${storedYaml.length} chars` : 'none'}
+
{storedYaml ? `${storedYaml.length} chars` : 'none'}
diff --git a/src/components/graph/FunctionNode.tsx b/src/components/graph/FunctionNode.tsx index 0964032..056e6f5 100644 --- a/src/components/graph/FunctionNode.tsx +++ b/src/components/graph/FunctionNode.tsx @@ -2,6 +2,7 @@ import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useSt import CodeMirror from '@uiw/react-codemirror' import { javascript } from '@codemirror/lang-javascript' import FlowContext from '../../lib/flowContext' +import { useTheme } from '../../lib/themeContext' import { BaseNode, BaseNodeContent, @@ -23,6 +24,7 @@ return args[0]; export const FunctionNode = memo(function FunctionNode({ id, data }: Props) { const [value, setValue] = useState(data?.body ?? DEFAULT_BODY) + const { theme } = useTheme() const ctx = useContext(FlowContext) const setNodes = ctx?.setNodes @@ -66,6 +68,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data }: Props) { - + ) } @@ -51,7 +51,7 @@ export function OutputHandle({ id }: NodeHandleProps) { justifyContent: 'center', }} > - + ) } diff --git a/src/components/graph/RenderingNode.tsx b/src/components/graph/RenderingNode.tsx index 3d74074..be9e33d 100644 --- a/src/components/graph/RenderingNode.tsx +++ b/src/components/graph/RenderingNode.tsx @@ -230,7 +230,7 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) {
{error.message}
) ) : ( -
{output}
+
{output}
)} diff --git a/src/lib/themeContext.tsx b/src/lib/themeContext.tsx new file mode 100644 index 0000000..b83301d --- /dev/null +++ b/src/lib/themeContext.tsx @@ -0,0 +1,83 @@ +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' + +const STORAGE_KEY = 'zui-theme' + +export type Theme = 'light' | 'dark' + +function readStored(): Theme | null { + try { + const s = localStorage.getItem(STORAGE_KEY) + if (s === 'light' || s === 'dark') return s + } catch (_) {} + return null +} + +function systemPrefersDark(): boolean { + try { + return window.matchMedia('(prefers-color-scheme: dark)').matches + } catch (_) { + return false + } +} + +type ThemeContextValue = { + theme: Theme + setTheme: (theme: Theme) => void + toggleTheme: () => void +} + +const ThemeContext = createContext(null) + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setThemeState] = useState(() => { + const stored = readStored() + if (stored) return stored + return systemPrefersDark() ? 'dark' : 'light' + }) + + useEffect(() => { + const root = document.documentElement + if (theme === 'dark') { + root.classList.add('dark') + } else { + root.classList.remove('dark') + } + }, [theme]) + + useEffect(() => { + const m = window.matchMedia('(prefers-color-scheme: dark)') + const handler = () => { + if (readStored() != null) return + setThemeState(m.matches ? 'dark' : 'light') + } + m.addEventListener('change', handler) + return () => m.removeEventListener('change', handler) + }, []) + + const setTheme = useCallback((next: Theme) => { + setThemeState(next) + try { + localStorage.setItem(STORAGE_KEY, next) + } catch (_) {} + }, []) + + const toggleTheme = useCallback(() => { + setThemeState((prev) => { + const next = prev === 'light' ? 'dark' : 'light' + try { + localStorage.setItem(STORAGE_KEY, next) + } catch (_) {} + return next + }) + }, []) + + const value = useMemo(() => ({ theme, setTheme, toggleTheme }), [theme, setTheme, toggleTheme]) + + return {children} +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext) + if (!ctx) throw new Error('useTheme must be used within ThemeProvider') + return ctx +} diff --git a/src/main.tsx b/src/main.tsx index 44e5a82..adac58b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,14 @@ import React from 'react' import { createRoot } from 'react-dom/client' import App from './App' +import { ThemeProvider } from './lib/themeContext' import './styles.css' -import 'reactflow/dist/style.css' +import '@xyflow/react/dist/style.css' createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/src/styles.css b/src/styles.css index bdf0a3f..045f8b9 100644 --- a/src/styles.css +++ b/src/styles.css @@ -11,7 +11,7 @@ body, } body { - @apply bg-gray-50 text-gray-900; + @apply bg-background text-foreground; } .reactflow-wrapper { @@ -28,6 +28,10 @@ body { box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06); } +.dark .react-flow__node .react-flow__handle { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.15); +} + /* Small helper for monospace pre output */ pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;