add theme
This commit is contained in:
31
src/App.tsx
31
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<any | null>(null)
|
||||
@@ -140,6 +142,16 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute top-3 right-3 z-10 nodrag nopan"
|
||||
onClick={toggleTheme}
|
||||
title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
@@ -151,6 +163,7 @@ export default function App() {
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
colorMode={theme as ColorMode}
|
||||
fitView
|
||||
onInit={onInit}
|
||||
>
|
||||
|
||||
@@ -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<string>(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) {
|
||||
</BaseNodeContent>
|
||||
|
||||
<BaseNodeFooter>
|
||||
<div className="w-full text-xs text-gray-500">{storedYaml ? `${storedYaml.length} chars` : 'none'}</div>
|
||||
<div className="w-full text-xs text-muted-foreground">{storedYaml ? `${storedYaml.length} chars` : 'none'}</div>
|
||||
</BaseNodeFooter>
|
||||
|
||||
<InputHandle id="ain" />
|
||||
|
||||
@@ -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<string>(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) {
|
||||
<CodeMirror
|
||||
value={value}
|
||||
height="120px"
|
||||
theme={theme}
|
||||
extensions={extensions}
|
||||
onChange={onChange}
|
||||
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Handle, Position } from 'reactflow'
|
||||
import { Handle, Position } from '@xyflow/react'
|
||||
import { Circle, CircleDashed } from 'lucide-react'
|
||||
|
||||
type NodeHandleProps = {
|
||||
@@ -26,7 +26,7 @@ export function InputHandle({ id }: NodeHandleProps) {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<CircleDashed className="w-4 h-4 text-gray-500 pointer-events-none" />
|
||||
<CircleDashed className="w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||
</Handle>
|
||||
)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export function OutputHandle({ id }: NodeHandleProps) {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Circle className="w-4 h-4 text-gray-800 pointer-events-none" />
|
||||
<Circle className="w-4 h-4 text-foreground pointer-events-none" />
|
||||
</Handle>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ export const RenderingNode = memo(function RenderingNode({ id }: Props) {
|
||||
<div className="p-3 text-xs text-red-700">{error.message}</div>
|
||||
)
|
||||
) : (
|
||||
<pre className="text-xs text-gray-800 max-h-48 overflow-auto">{output}</pre>
|
||||
<pre className="text-xs text-foreground max-h-48 overflow-auto">{output}</pre>
|
||||
)}
|
||||
</BaseNodeContent>
|
||||
|
||||
|
||||
83
src/lib/themeContext.tsx
Normal file
83
src/lib/themeContext.tsx
Normal file
@@ -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<ThemeContextValue | null>(null)
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
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 <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export function useTheme(): ThemeContextValue {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
|
||||
return ctx
|
||||
}
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user