diff --git a/blendmate-app/src/App.tsx b/blendmate-app/src/App.tsx index 9e7a9ab..4c42f9b 100644 --- a/blendmate-app/src/App.tsx +++ b/blendmate-app/src/App.tsx @@ -1,13 +1,10 @@ import { useState, useEffect } from "react"; import { useBlendmateSocket } from "./useBlendmateSocket"; -import { Card } from "@/components/ui/card"; -import {BackgroundPaths} from "@/components/ui/BackgroundPaths"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import Outliner from "./components/Outliner"; -import NodeHelpView from "./components/NodeHelpView"; -import EventsLogPanel from "./components/panels/EventsLogPanel"; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Outliner, NodeHelpView } from "@/components"; +import { EventsLogPanel } from "@/components/panels"; import { Activity, LayoutGrid, Info, ListTree } from "lucide-react"; +import BackgroundPaths from "@/components/ui/BackgroundPaths"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup, Card, ScrollArea } from "@/components/ui"; // Color tokens (increased opacity for debug visibility) diff --git a/blendmate-app/src/app/AppShell.tsx b/blendmate-app/src/app/AppShell.tsx new file mode 100644 index 0000000..8465f7c --- /dev/null +++ b/blendmate-app/src/app/AppShell.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { TopBar } from "./layout/TopBar"; +import { CenterWorkspace } from "./layout/CenterWorkspace"; +import { RightInspector } from "./layout/RightInspector"; +import { BottomBar } from "./layout/BottomBar"; +import { useLayoutStore } from "../state/layoutStore"; + +export type SocketStatus = "connecting" | "connected" | "disconnected" | string; + +export type AppShellProps = { + socketStatus: SocketStatus; + lastMessage: any; + sendJson: (json: any) => void; +}; + +export function AppShell(props: AppShellProps) { + const layout = useLayoutStore(); + + useEffect(() => { + // Sanity marker: guarantees you're seeing the vNext shell build. + // eslint-disable-next-line no-console + console.info("Blendmate UI vNext Shell mounted"); + }, []); + + return ( +
+ + +
+
+ +
+ + {layout.inspectorOpen ? ( +
+ +
+ ) : null} +
+ + {layout.bottomOpen ? ( +
+ +
+ ) : ( +
+ Bottom bar hidden + +
+ )} +
+ ); +} diff --git a/blendmate-app/src/app/layout/BottomBar.tsx b/blendmate-app/src/app/layout/BottomBar.tsx new file mode 100644 index 0000000..9984fa3 --- /dev/null +++ b/blendmate-app/src/app/layout/BottomBar.tsx @@ -0,0 +1,60 @@ +import { useMemo, useState } from "react"; + +export type BottomBarProps = { + lastMessage: any; + sendJson: (json: any) => void; + onToggleBottom: () => void; + vnextBadge?: boolean; +}; + +export function BottomBar(props: BottomBarProps) { + const [prompt, setPrompt] = useState(""); + + const last = useMemo(() => { + if (!props.lastMessage) return "Awaiting Blender signal…"; + try { + return "[RECV] " + JSON.stringify(props.lastMessage); + } catch { + return "[RECV] (unserializable message)"; + } + }, [props.lastMessage]); + + return ( +
+ {props.vnextBadge ? ( + vNext + ) : null} + + + +
+ setPrompt(e.target.value)} + /> +
+ + + +
+ {last} +
+
+ ); +} diff --git a/blendmate-app/src/app/layout/CenterWorkspace.tsx b/blendmate-app/src/app/layout/CenterWorkspace.tsx new file mode 100644 index 0000000..024ca37 --- /dev/null +++ b/blendmate-app/src/app/layout/CenterWorkspace.tsx @@ -0,0 +1,21 @@ +export function CenterWorkspace() { + return ( +
+
+
Workspace
+
tabs/splits later
+
+ +
+
+
+
Sandbox canvas
+
+ This is the vNext shell baseline. We'll add tabs, panels, and workflows on top of this. +
+
+
+
+
+ ); +} diff --git a/blendmate-app/src/app/layout/RightInspector.tsx b/blendmate-app/src/app/layout/RightInspector.tsx new file mode 100644 index 0000000..6e925c4 --- /dev/null +++ b/blendmate-app/src/app/layout/RightInspector.tsx @@ -0,0 +1,13 @@ +export function RightInspector() { + return ( +
+
Inspector
+
+
Nothing selected
+
+ Contextual help will appear here (tools, nodes, selection mode, shortcuts…). +
+
+
+ ); +} diff --git a/blendmate-app/src/app/layout/TopBar.tsx b/blendmate-app/src/app/layout/TopBar.tsx new file mode 100644 index 0000000..7d54fa3 --- /dev/null +++ b/blendmate-app/src/app/layout/TopBar.tsx @@ -0,0 +1,62 @@ +import type { SocketStatus } from "../AppShell"; + +export type TopBarProps = { + socketStatus: SocketStatus; + inspectorOpen: boolean; + onToggleInspector: () => void; + onResetLayout: () => void; +}; + +function statusLabel(s: SocketStatus) { + if (s === "connected") return "Connected"; + if (s === "connecting") return "Connecting…"; + if (s === "disconnected") return "Disconnected"; + return String(s || "Unknown"); +} + +export function TopBar(props: TopBarProps) { + const connected = props.socketStatus === "connected"; + + return ( +
+
+
+ Blendmate +
+ + vNext Shell + +
+ +
+
+ + {statusLabel(props.socketStatus)} +
+ + + + +
+
+ ); +} diff --git a/blendmate-app/src/components/index.ts b/blendmate-app/src/components/index.ts new file mode 100644 index 0000000..ec953a8 --- /dev/null +++ b/blendmate-app/src/components/index.ts @@ -0,0 +1,5 @@ +// Barrel file for components +export { default as Outliner } from "./Outliner"; +export { default as NodeHelpView } from "./NodeHelpView"; +export * from "./ui"; + diff --git a/blendmate-app/src/components/panels/index.ts b/blendmate-app/src/components/panels/index.ts new file mode 100644 index 0000000..bc2587d --- /dev/null +++ b/blendmate-app/src/components/panels/index.ts @@ -0,0 +1,5 @@ +export { default as ChatPanel } from "./ChatPanel"; +export { default as EventsLogPanel } from "./EventsLogPanel"; +export { default as NodesHelpPanel } from "./NodesHelpPanel"; +export { default as StatsPanel } from "./StatsPanel"; + diff --git a/blendmate-app/src/components/ui/BackgroundPaths_Backup.tsx b/blendmate-app/src/components/ui/BackgroundPaths_Backup.tsx deleted file mode 100644 index 99aec48..0000000 --- a/blendmate-app/src/components/ui/BackgroundPaths_Backup.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useMemo, memo } from "react"; - -interface BackgroundPathsProps { - color?: string; - className?: string; -} - -function BackgroundPathsComponent({ color = "rgba(99,102,241,0.18)", className = "" }: BackgroundPathsProps) { - // Procedurally generate multiple layers of paths with slight randomness and different animations - const width = 1400; - const height = 900; - - // simple deterministic PRNG with better distribution - function makeRng(seed: number) { - let s = seed % 2147483647; - if (s <= 0) s += 2147483646; - return () => { - s = (s * 48271) % 2147483647; // standard Lehmer generator - return (s - 1) / 2147483646; - }; - } - - // Build a multi-segment wavy cubic path between startX and endX. - function makeWavyPath(startX: number, yStart: number, endX: number, yEnd: number, rng: () => number, amp: number, segments = 5) { - const span = endX - startX; - const pts: Array<{ x: number; y: number }> = []; - pts.push({ x: startX, y: yStart }); - - for (let s = 1; s < segments; s++) { - const t = s / segments; - const x = startX + span * t; - const yBase = yStart + (yEnd - yStart) * t; - // Use simpler wave generation to reduce complexity - const wave = Math.sin(t * Math.PI * 2 + rng() * Math.PI) * amp; - const jitter = (rng() - 0.5) * amp * 0.2; - pts.push({ x, y: yBase + wave + jitter }); - } - pts.push({ x: endX, y: yEnd }); - - let d = `M${pts[0].x.toFixed(1)},${pts[0].y.toFixed(1)}`; - for (let i = 0; i < pts.length - 1; i++) { - const p0 = pts[i]; - const p1 = pts[i + 1]; - const dx = p1.x - p0.x; - const cp1x = (p0.x + dx * 0.38).toFixed(1); - const cp1y = (p0.y + (rng() - 0.5) * amp * 0.5).toFixed(1); - const cp2x = (p0.x + dx * 0.62).toFixed(1); - const cp2y = (p1.y + (rng() - 0.5) * amp * 0.5).toFixed(1); - d += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${p1.x.toFixed(1)},${p1.y.toFixed(1)}`; - } - return d; - } - - const layerPaths = useMemo(() => { - const layers = [ - { count: 22, strokeWidth: 1, opacity: 0.8, anim: 'bm-anim-slow', amp: 180, blur: true }, - { count: 12, strokeWidth: 1, opacity: 0.9, anim: 'bm-anim-med', amp: 120 }, - { count: 43, strokeWidth: 1, opacity: 0.7, anim: 'bm-anim-fast', amp: 90 }, - ]; - - return layers.map((layer, li) => { - const rng = makeRng(2000 + li * 137); - const paths: Array<{ d: string; delay: string; strokeWidth: number; opacity: number; duration: string }> = []; - const baseY = height * 0.5; - - for (let i = 0; i < layer.count; i++) { - const t = i / Math.max(1, layer.count - 1); - const yStart = baseY + (t - 0.5) * 600 + (rng() - 0.5) * 100; - const yEnd = yStart + (rng() - 0.5) * 150; - - const EXTENT = width * 1.5; - const startX = -EXTENT; - const endX = width + EXTENT; - - const segments = Math.max(3, Math.min(5, Math.round(layer.amp / 40))); - const d = makeWavyPath(startX, yStart, endX, yEnd, rng, layer.amp, segments); - - const delay = `${-(rng() * 60).toFixed(2)}s`; - const baseDur = layer.anim === 'bm-anim-slow' ? 60 : layer.anim === 'bm-anim-med' ? 45 : 30; - const dur = baseDur * (0.8 + rng() * 0.4); - const duration = `${dur.toFixed(2)}s`; - - const sw = layer.strokeWidth * (0.8 + rng() * 0.4); - const op = layer.opacity * (0.8 + rng() * 0.4); - paths.push({ d, delay, duration, strokeWidth: Number(sw.toFixed(1)), opacity: Number(op.toFixed(3)) }); - } - return { layer, paths }; - }); - }, []); - - return ( -
- - - - - - - - - - - - - {layerPaths.map(({ layer, paths }, li) => ( - - {paths.map((p, pi) => ( - - ))} - - ))} - -
- ); -} - -const BackgroundPaths = memo(BackgroundPathsComponent); -export default BackgroundPaths; diff --git a/blendmate-app/src/components/ui/IslandPanel.tsx b/blendmate-app/src/components/ui/IslandPanel.tsx index 3f20e03..b5faa0f 100644 --- a/blendmate-app/src/components/ui/IslandPanel.tsx +++ b/blendmate-app/src/components/ui/IslandPanel.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardAction } from "@/components/ui/card"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardAction } from "@/components/ui/Card"; import { cn } from "@/lib/utils"; interface IslandPanelProps { diff --git a/blendmate-app/src/components/ui/__tests__/BackgroundPaths.test.tsx b/blendmate-app/src/components/ui/__tests__/BackgroundPaths.test.tsx deleted file mode 100644 index f69e1ac..0000000 --- a/blendmate-app/src/components/ui/__tests__/BackgroundPaths.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { render } from '@testing-library/react'; -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; -import BackgroundPaths from '../BackgroundPaths'; - -// Mock Math.random to be deterministic for test -const randomValues: number[] = []; -let rvIndex = 0; -const seededRandom = () => { - if (rvIndex >= randomValues.length) rvIndex = 0; - return randomValues[rvIndex++]; -}; - -describe('BackgroundPaths', () => { - beforeEach(() => { - // deterministic sequence: alternate values to produce different x/y/props - randomValues.length = 0; - randomValues.push(0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9); - rvIndex = 0; - vi.spyOn(Math, 'random').mockImplementation(seededRandom as any); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('renders expected number of streak groups and paths with quadratic bezier', () => { - const count = 6; - render(); - - // čekáme count elementů - const groups = document.querySelectorAll('g[data-debug="true"]'); - expect(groups.length).toBe(count); - - // Každá skupina by měla obsahovat s atributem stroke a křivkou Q - groups.forEach((g) => { - const path = g.querySelector('path'); - expect(path).toBeTruthy(); - if (path) { - const d = path.getAttribute('d') || ''; - expect(d.includes('Q')).toBe(true); - expect(path.getAttribute('stroke')).toBe('rgb(255,0,0)'); - } - }); - }); -}); diff --git a/blendmate-app/src/components/ui/index.ts b/blendmate-app/src/components/ui/index.ts new file mode 100644 index 0000000..4f19158 --- /dev/null +++ b/blendmate-app/src/components/ui/index.ts @@ -0,0 +1,11 @@ +// Barrel file for ui components +export { BackgroundPaths } from "./BackgroundPaths"; +export type { BackgroundPathsProps } from "./BackgroundPaths"; +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent, CardAction } from "./Card"; +export { default as IslandPanel } from "./IslandPanel"; +export { ScrollArea, ScrollBar } from "./scroll-area"; +export { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "./resizable"; +export { Badge } from "./badge"; +export { Button } from "./button"; +export { Input } from "./input"; +export { Skeleton } from "./skeleton"; diff --git a/blendmate-app/src/state/layoutStore.ts b/blendmate-app/src/state/layoutStore.ts new file mode 100644 index 0000000..1e709fd --- /dev/null +++ b/blendmate-app/src/state/layoutStore.ts @@ -0,0 +1,47 @@ +import { useEffect, useMemo, useState } from "react"; + +type LayoutState = { + inspectorOpen: boolean; + bottomOpen: boolean; +}; + +const KEY = "blendmate.layout.vnext"; + +function readInitial(): LayoutState { + try { + const raw = localStorage.getItem(KEY); + if (!raw) return { inspectorOpen: true, bottomOpen: true }; + const parsed = JSON.parse(raw); + return { + inspectorOpen: Boolean(parsed.inspectorOpen), + bottomOpen: Boolean(parsed.bottomOpen), + }; + } catch { + return { inspectorOpen: true, bottomOpen: true }; + } +} + +export function useLayoutStore() { + const [state, setState] = useState(() => readInitial()); + + useEffect(() => { + try { + localStorage.setItem(KEY, JSON.stringify(state)); + } catch { + // ignore + } + }, [state]); + + return useMemo( + () => ({ + inspectorOpen: state.inspectorOpen, + bottomOpen: state.bottomOpen, + toggleInspector: () => + setState((s) => ({ ...s, inspectorOpen: !s.inspectorOpen })), + toggleBottom: () => + setState((s) => ({ ...s, bottomOpen: !s.bottomOpen })), + resetLayout: () => setState({ inspectorOpen: true, bottomOpen: true }), + }), + [state] + ); +} diff --git a/vnext-shell.patch b/vnext-shell.patch new file mode 100644 index 0000000..1f4a981 --- /dev/null +++ b/vnext-shell.patch @@ -0,0 +1,355 @@ +diff --git a/blendmate-app/src/App.tsx b/blendmate-app/src/App.tsx +index 1111111..2222222 100644 +--- a/blendmate-app/src/App.tsx ++++ b/blendmate-app/src/App.tsx +@@ -1,200 +1,34 @@ +-import React, { useEffect, useMemo, useState } from "react"; +-import HUD from "./components/layout/HUD"; +-import Footer from "./components/layout/Footer"; +-import SandboxLayout from "./components/layout/SandboxLayout"; +-import { useBlendmateSocket } from "./useBlendmateSocket"; +-import { usePanelManager } from "./usePanelManager"; +- +-export default function App() { +- // legacy/test orchestration... +- // (previous sandbox layout and event/state wiring) +- return ( +-
+- +- +-
+-
+- ); +-} ++import React from "react"; ++import { useBlendmateSocket } from "./useBlendmateSocket"; ++import { AppShell } from "./app/AppShell"; ++ ++/** ++ * App = plumbing wrapper only. ++ * UI layout lives in src/app/AppShell.tsx (vNext). ++ */ ++export default function App() { ++ const { status, lastMessage, sendJson } = useBlendmateSocket(); ++ ++ return ( ++ ++ ); ++} +diff --git a/blendmate-app/src/app/AppShell.tsx b/blendmate-app/src/app/AppShell.tsx +new file mode 100644 +index 0000000..3333333 +--- /dev/null ++++ b/blendmate-app/src/app/AppShell.tsx +@@ -0,0 +1,96 @@ ++import React, { useEffect } from "react"; ++import { TopBar } from "./layout/TopBar"; ++import { CenterWorkspace } from "./layout/CenterWorkspace"; ++import { RightInspector } from "./layout/RightInspector"; ++import { BottomBar } from "./layout/BottomBar"; ++import { useLayoutStore } from "../state/layoutStore"; ++ ++export type SocketStatus = "connecting" | "connected" | "disconnected" | string; ++ ++export type AppShellProps = { ++ socketStatus: SocketStatus; ++ lastMessage: any; ++ sendJson: (json: any) => void; ++}; ++ ++export function AppShell(props: AppShellProps) { ++ const layout = useLayoutStore(); ++ ++ useEffect(() => { ++ // Sanity marker: guarantees you're seeing the vNext shell build. ++ // eslint-disable-next-line no-console ++ console.info("Blendmate UI vNext Shell mounted"); ++ }, []); ++ ++ return ( ++
++ ++ ++
++
++ ++
++ ++ {layout.inspectorOpen ? ( ++
++ ++
++ ) : null} ++
++ ++ {layout.bottomOpen ? ( ++
++ ++
++ ) : ( ++
++ Bottom bar hidden ++ ++
++ )} ++
++ ); ++} +diff --git a/blendmate-app/src/app/layout/TopBar.tsx b/blendmate-app/src/app/layout/TopBar.tsx +new file mode 100644 +index 0000000..4444444 +--- /dev/null ++++ b/blendmate-app/src/app/layout/TopBar.tsx +@@ -0,0 +1,76 @@ ++import React from "react"; ++import type { SocketStatus } from "../AppShell"; ++ ++export type TopBarProps = { ++ socketStatus: SocketStatus; ++ inspectorOpen: boolean; ++ onToggleInspector: () => void; ++ onResetLayout: () => void; ++}; ++ ++function statusLabel(s: SocketStatus) { ++ if (s === "connected") return "Connected"; ++ if (s === "connecting") return "Connecting&"; ++ if (s === "disconnected") return "Disconnected"; ++ return String(s || "Unknown"); ++} ++ ++export function TopBar(props: TopBarProps) { ++ const connected = props.socketStatus === "connected"; ++ ++ return ( ++
++
++
++ Blendmate ++
++ ++ vNext Shell ++ ++
++ ++
++
++ ++ {statusLabel(props.socketStatus)} ++
++ ++ ++ ++ ++
++
++ ); ++} +diff --git a/blendmate-app/src/app/layout/CenterWorkspace.tsx b/blendmate-app/src/app/layout/CenterWorkspace.tsx +new file mode 100644 +index 0000000..5555555 +--- /dev/null ++++ b/blendmate-app/src/app/layout/CenterWorkspace.tsx +@@ -0,0 +1,42 @@ ++import React from "react"; ++ ++export function CenterWorkspace() { ++ return ( ++
++
++
Workspace
++
tabs/splits later
++
++ ++
++
++
++
Sandbox canvas
++
++ This is the vNext shell baseline. We'll add tabs, panels, and workflows on top of this. ++
++
++
++
++
++ ); ++} +diff --git a/blendmate-app/src/app/layout/RightInspector.tsx b/blendmate-app/src/app/layout/RightInspector.tsx +new file mode 100644 +index 0000000..6666666 +--- /dev/null ++++ b/blendmate-app/src/app/layout/RightInspector.tsx +@@ -0,0 +1,33 @@ ++import React from "react"; ++ ++export function RightInspector() { ++ return ( ++
++
Inspector
++
++
Nothing selected
++
++ Contextual help will appear here (tools, nodes, selection mode, shortcuts&). ++
++
++
++ ); ++} +diff --git a/blendmate-app/src/app/layout/BottomBar.tsx b/blendmate-app/src/app/layout/BottomBar.tsx +new file mode 100644 +index 0000000..7777777 +--- /dev/null ++++ b/blendmate-app/src/app/layout/BottomBar.tsx +@@ -0,0 +1,79 @@ ++import React, { useMemo, useState } from "react"; ++ ++export type BottomBarProps = { ++ lastMessage: any; ++ sendJson: (json: any) => void; ++ onToggleBottom: () => void; ++ vnextBadge?: boolean; ++}; ++ ++export function BottomBar(props: BottomBarProps) { ++ const [prompt, setPrompt] = useState(""); ++ ++ const last = useMemo(() => { ++ if (!props.lastMessage) return "Awaiting Blender signal&"; ++ try { ++ return "[RECV] " + JSON.stringify(props.lastMessage); ++ } catch { ++ return "[RECV] (unserializable message)"; ++ } ++ }, [props.lastMessage]); ++ ++ return ( ++
++ {props.vnextBadge ? ( ++ vNext ++ ) : null} ++ ++ ++ ++
++ setPrompt(e.target.value)} ++ /> ++
++ ++ ++ ++
++ {last} ++
++
++ ); ++} +diff --git a/blendmate-app/src/state/layoutStore.ts b/blendmate-app/src/state/layoutStore.ts +new file mode 100644 +index 0000000..8888888 +--- /dev/null ++++ b/blendmate-app/src/state/layoutStore.ts +@@ -0,0 +1,71 @@ ++import { useEffect, useMemo, useState } from "react"; ++ ++type LayoutState = { ++ inspectorOpen: boolean; ++ bottomOpen: boolean; ++}; ++ ++const KEY = "blendmate.layout.vnext"; ++ ++function readInitial(): LayoutState { ++ try { ++ const raw = localStorage.getItem(KEY); ++ if (!raw) return { inspectorOpen: true, bottomOpen: true }; ++ const parsed = JSON.parse(raw); ++ return { ++ inspectorOpen: Boolean(parsed.inspectorOpen), ++ bottomOpen: Boolean(parsed.bottomOpen), ++ }; ++ } catch { ++ return { inspectorOpen: true, bottomOpen: true }; ++ } ++} ++ ++export function useLayoutStore() { ++ const [state, setState] = useState(() => readInitial()); ++ ++ useEffect(() => { ++ try { ++ localStorage.setItem(KEY, JSON.stringify(state)); ++ } catch { ++ // ignore ++ } ++ }, [state]); ++ ++ return useMemo( ++ () => ({ ++ inspectorOpen: state.inspectorOpen, ++ bottomOpen: state.bottomOpen, ++ toggleInspector: () => ++ setState((s) => ({ ...s, inspectorOpen: !s.inspectorOpen })), ++ toggleBottom: () => ++ setState((s) => ({ ...s, bottomOpen: !s.bottomOpen })), ++ resetLayout: () => setState({ inspectorOpen: true, bottomOpen: true }), ++ }), ++ [state] ++ ); ++}