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 (
-
-
-
- );
-}
-
-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]
++ );
++}