add theme
This commit is contained in:
31
src/App.tsx
31
src/App.tsx
@@ -1,24 +1,23 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactFlow, {
|
import {
|
||||||
|
ReactFlow,
|
||||||
Controls,
|
Controls,
|
||||||
Background,
|
Background,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
addEdge,
|
addEdge,
|
||||||
applyEdgeChanges,
|
|
||||||
applyNodeChanges,
|
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
Node,
|
type Node,
|
||||||
Edge,
|
type Edge,
|
||||||
Connection,
|
type Connection,
|
||||||
NodeChange,
|
type ColorMode,
|
||||||
EdgeChange,
|
} from '@xyflow/react'
|
||||||
} from 'reactflow'
|
|
||||||
import ConfigNode from './components/graph/ConfigNode'
|
import ConfigNode from './components/graph/ConfigNode'
|
||||||
import FunctionNode from './components/graph/FunctionNode'
|
import FunctionNode from './components/graph/FunctionNode'
|
||||||
import RenderingNode from './components/graph/RenderingNode'
|
import RenderingNode from './components/graph/RenderingNode'
|
||||||
import VariableNode from './components/graph/VariableNode'
|
import VariableNode from './components/graph/VariableNode'
|
||||||
import FlowContext from './lib/flowContext'
|
import FlowContext from './lib/flowContext'
|
||||||
|
import { useTheme } from './lib/themeContext'
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
@@ -27,6 +26,8 @@ import {
|
|||||||
ContextMenuLabel,
|
ContextMenuLabel,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu"
|
} from "@/components/ui/context-menu"
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Moon, Sun } from 'lucide-react'
|
||||||
// Generate short unique node ids when missing
|
// Generate short unique node ids when missing
|
||||||
const genId = () => `node_${Math.random().toString(36).slice(2, 9)}`
|
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' }]
|
const initialEdges: Edge[] = [{ id: 'e-config-render', source: 'config-1', target: 'render-1' }]
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { theme, toggleTheme } = useTheme()
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
||||||
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
const [rfInstance, setRfInstance] = React.useState<any | null>(null)
|
||||||
@@ -140,6 +142,16 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="reactflow-wrapper" ref={wrapperRef} onContextMenu={onCanvasContextMenu} style={{ position: 'relative' }}>
|
<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 }}>
|
<FlowContext.Provider value={{ nodes, setNodes, edges, setEdges }}>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
@@ -151,6 +163,7 @@ export default function App() {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
|
colorMode={theme as ColorMode}
|
||||||
fitView
|
fitView
|
||||||
onInit={onInit}
|
onInit={onInit}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useSt
|
|||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { yaml } from '@codemirror/lang-yaml'
|
import { yaml } from '@codemirror/lang-yaml'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { useTheme } from '../../lib/themeContext'
|
||||||
import {
|
import {
|
||||||
BaseNode,
|
BaseNode,
|
||||||
BaseNodeContent,
|
BaseNodeContent,
|
||||||
@@ -29,7 +30,7 @@ type Props = {
|
|||||||
|
|
||||||
export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
||||||
const [value, setValue] = useState<string>(data?.yaml ?? '# Enter YAML here\n')
|
const [value, setValue] = useState<string>(data?.yaml ?? '# Enter YAML here\n')
|
||||||
|
const { theme } = useTheme()
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const setNodes = ctx?.setNodes
|
const setNodes = ctx?.setNodes
|
||||||
const storedYaml = ctx?.nodes?.find((n: any) => n.id === id)?.data?.yaml ?? ''
|
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}
|
ref={editorRef}
|
||||||
value={value}
|
value={value}
|
||||||
height="180px"
|
height="180px"
|
||||||
|
theme={theme}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
||||||
@@ -208,7 +210,7 @@ export const ConfigNode = memo(function ConfigNode({ id, data }: Props) {
|
|||||||
</BaseNodeContent>
|
</BaseNodeContent>
|
||||||
|
|
||||||
<BaseNodeFooter>
|
<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>
|
</BaseNodeFooter>
|
||||||
|
|
||||||
<InputHandle id="ain" />
|
<InputHandle id="ain" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useSt
|
|||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { javascript } from '@codemirror/lang-javascript'
|
import { javascript } from '@codemirror/lang-javascript'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { useTheme } from '../../lib/themeContext'
|
||||||
import {
|
import {
|
||||||
BaseNode,
|
BaseNode,
|
||||||
BaseNodeContent,
|
BaseNodeContent,
|
||||||
@@ -23,6 +24,7 @@ return args[0];
|
|||||||
|
|
||||||
export const FunctionNode = memo(function FunctionNode({ id, data }: Props) {
|
export const FunctionNode = memo(function FunctionNode({ id, data }: Props) {
|
||||||
const [value, setValue] = useState<string>(data?.body ?? DEFAULT_BODY)
|
const [value, setValue] = useState<string>(data?.body ?? DEFAULT_BODY)
|
||||||
|
const { theme } = useTheme()
|
||||||
const ctx = useContext(FlowContext)
|
const ctx = useContext(FlowContext)
|
||||||
const setNodes = ctx?.setNodes
|
const setNodes = ctx?.setNodes
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ export const FunctionNode = memo(function FunctionNode({ id, data }: Props) {
|
|||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
height="120px"
|
height="120px"
|
||||||
|
theme={theme}
|
||||||
extensions={extensions}
|
extensions={extensions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
basicSetup={{ lineNumbers: true, foldGutter: false }}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Handle, Position } from 'reactflow'
|
import { Handle, Position } from '@xyflow/react'
|
||||||
import { Circle, CircleDashed } from 'lucide-react'
|
import { Circle, CircleDashed } from 'lucide-react'
|
||||||
|
|
||||||
type NodeHandleProps = {
|
type NodeHandleProps = {
|
||||||
@@ -26,7 +26,7 @@ export function InputHandle({ id }: NodeHandleProps) {
|
|||||||
justifyContent: 'center',
|
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>
|
</Handle>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ export function OutputHandle({ id }: NodeHandleProps) {
|
|||||||
justifyContent: 'center',
|
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>
|
</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>
|
<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>
|
</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 React from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { ThemeProvider } from './lib/themeContext'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
import 'reactflow/dist/style.css'
|
import '@xyflow/react/dist/style.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-gray-50 text-gray-900;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reactflow-wrapper {
|
.reactflow-wrapper {
|
||||||
@@ -28,6 +28,10 @@ body {
|
|||||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
|
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 */
|
/* Small helper for monospace pre output */
|
||||||
pre {
|
pre {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;
|
||||||
|
|||||||
Reference in New Issue
Block a user