From d4559720f8c2ed3487c3e49df4cad3ed7bb3e009 Mon Sep 17 00:00:00 2001
From: Camiel van Schoonhoven
Date: Wed, 11 Feb 2026 15:25:29 -0800
Subject: [PATCH] hackdays: Factory Game Scaffold
---
src/components/Factory/Canvas/GameCanvas.tsx | 86 +++++++++++++++++++
.../Factory/Canvas/GameControls.tsx | 66 ++++++++++++++
src/components/Factory/FactoryGame.tsx | 54 ++++++++++++
src/components/Factory/Sidebar/Buildings.tsx | 5 ++
src/components/Factory/Sidebar/Controls.tsx | 5 ++
.../Factory/Sidebar/GameSidebar.tsx | 40 +++++++++
src/components/Factory/Sidebar/Resources.tsx | 5 ++
src/components/layout/AppMenu.tsx | 14 ++-
src/routes/Factory/FactoryGame.tsx | 18 ++++
src/routes/router.ts | 9 ++
10 files changed, 301 insertions(+), 1 deletion(-)
create mode 100644 src/components/Factory/Canvas/GameCanvas.tsx
create mode 100644 src/components/Factory/Canvas/GameControls.tsx
create mode 100644 src/components/Factory/FactoryGame.tsx
create mode 100644 src/components/Factory/Sidebar/Buildings.tsx
create mode 100644 src/components/Factory/Sidebar/Controls.tsx
create mode 100644 src/components/Factory/Sidebar/GameSidebar.tsx
create mode 100644 src/components/Factory/Sidebar/Resources.tsx
create mode 100644 src/routes/Factory/FactoryGame.tsx
diff --git a/src/components/Factory/Canvas/GameCanvas.tsx b/src/components/Factory/Canvas/GameCanvas.tsx
new file mode 100644
index 000000000..666f12538
--- /dev/null
+++ b/src/components/Factory/Canvas/GameCanvas.tsx
@@ -0,0 +1,86 @@
+import {
+ type Connection,
+ type Edge,
+ type Node,
+ type OnInit,
+ ReactFlow,
+ type ReactFlowInstance,
+ type ReactFlowProps,
+ useEdgesState,
+ useNodesState,
+} from "@xyflow/react";
+import type { ComponentType } from "react";
+import { useState } from "react";
+
+import { BlockStack } from "@/components/ui/layout";
+
+const nodeTypes: Record> = {
+ building: () => (
+
+ ),
+};
+
+const edgeTypes: Record> = {
+ resourceEdge: () => null, // Will use default edge for now
+};
+
+const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {
+ const [nodes, , onNodesChange] = useNodesState([]);
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+ const [, setReactFlowInstance] = useState();
+
+ const onInit: OnInit = (instance) => {
+ setReactFlowInstance(instance);
+ instance.fitView({ maxZoom: 1 });
+ };
+
+ const onConnect = (connection: Connection) => {
+ if (connection.source === connection.target) return;
+
+ setEdges((eds) => [
+ ...eds,
+ {
+ ...connection,
+ id: `${connection.source}-${connection.target}`,
+ type: "resourceEdge",
+ } as Edge,
+ ]);
+ };
+
+ const onNodesDelete = (deleted: Node[]) => {
+ console.log("Nodes deleted:", deleted);
+ };
+
+ const onEdgesDelete = (deleted: Edge[]) => {
+ console.log("Edges deleted:", deleted);
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default GameCanvas;
diff --git a/src/components/Factory/Canvas/GameControls.tsx b/src/components/Factory/Canvas/GameControls.tsx
new file mode 100644
index 000000000..dea742ebb
--- /dev/null
+++ b/src/components/Factory/Canvas/GameControls.tsx
@@ -0,0 +1,66 @@
+import {
+ ControlButton,
+ type ControlProps,
+ Controls,
+ type ReactFlowProps,
+} from "@xyflow/react";
+import {
+ LockKeyhole,
+ LockKeyholeOpen,
+ SquareDashedMousePointerIcon,
+} from "lucide-react";
+import { useCallback, useState } from "react";
+
+import { cn } from "@/lib/utils";
+
+interface GameControlsProps extends ControlProps {
+ config: ReactFlowProps;
+ updateConfig: (config: Partial) => void;
+}
+
+export default function GameControls({
+ config,
+ updateConfig,
+ ...props
+}: GameControlsProps) {
+ const [multiSelectActive, setMultiSelectActive] = useState(false);
+ const [lockActive, setLockActive] = useState(!config.nodesDraggable);
+
+ const onClickMultiSelect = useCallback(() => {
+ updateConfig({
+ selectionOnDrag: !multiSelectActive,
+ panOnDrag: multiSelectActive,
+ });
+ setMultiSelectActive(!multiSelectActive);
+ }, [multiSelectActive, updateConfig]);
+
+ const handleLockChange = useCallback(() => {
+ updateConfig({
+ nodesDraggable: lockActive,
+ });
+ setLockActive(!lockActive);
+ }, [lockActive, updateConfig]);
+
+ return (
+
+ {!props.showInteractive && (
+
+ {lockActive ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/Factory/FactoryGame.tsx b/src/components/Factory/FactoryGame.tsx
new file mode 100644
index 000000000..2850fa62d
--- /dev/null
+++ b/src/components/Factory/FactoryGame.tsx
@@ -0,0 +1,54 @@
+import "@/styles/editor.css";
+
+import { Background, MiniMap, type ReactFlowProps } from "@xyflow/react";
+import { useState } from "react";
+
+import { CollapsibleContextPanel } from "@/components/shared/ContextPanel/CollapsibleContextPanel";
+import { BlockStack, InlineStack } from "@/components/ui/layout";
+import { ContextPanelProvider } from "@/providers/ContextPanelProvider";
+
+import GameCanvas from "./Canvas/GameCanvas";
+import GameControls from "./Canvas/GameControls";
+import GameSidebar from "./Sidebar/GameSidebar";
+
+const GRID_SIZE = 10;
+
+const FactoryGame = () => {
+ const [flowConfig, setFlowConfig] = useState({
+ snapGrid: [GRID_SIZE, GRID_SIZE],
+ snapToGrid: true,
+ panOnDrag: true,
+ selectionOnDrag: false,
+ nodesDraggable: true,
+ });
+
+ const updateFlowConfig = (updatedConfig: Partial) => {
+ setFlowConfig((prevConfig) => ({
+ ...prevConfig,
+ ...updatedConfig,
+ }));
+ };
+
+ return (
+ Factory Game
}>
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FactoryGame;
diff --git a/src/components/Factory/Sidebar/Buildings.tsx b/src/components/Factory/Sidebar/Buildings.tsx
new file mode 100644
index 000000000..25d0d66ed
--- /dev/null
+++ b/src/components/Factory/Sidebar/Buildings.tsx
@@ -0,0 +1,5 @@
+const Buildings = () => {
+ return Building List
;
+};
+
+export default Buildings;
diff --git a/src/components/Factory/Sidebar/Controls.tsx b/src/components/Factory/Sidebar/Controls.tsx
new file mode 100644
index 000000000..a94e7d252
--- /dev/null
+++ b/src/components/Factory/Sidebar/Controls.tsx
@@ -0,0 +1,5 @@
+const Controls = () => {
+ return Next Turn
;
+};
+
+export default Controls;
diff --git a/src/components/Factory/Sidebar/GameSidebar.tsx b/src/components/Factory/Sidebar/GameSidebar.tsx
new file mode 100644
index 000000000..d4dea5250
--- /dev/null
+++ b/src/components/Factory/Sidebar/GameSidebar.tsx
@@ -0,0 +1,40 @@
+import { BlockStack } from "@/components/ui/layout";
+import { VerticalResizeHandle } from "@/components/ui/resize-handle";
+import { BOTTOM_FOOTER_HEIGHT, TOP_NAV_HEIGHT } from "@/utils/constants";
+
+import Buildings from "./Buildings";
+import Controls from "./Controls";
+import Resources from "./Resources";
+
+const MIN_WIDTH = 220;
+const MAX_WIDTH = 400;
+const DEFAULT_WIDTH = 256;
+
+const GameSidebar = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default GameSidebar;
diff --git a/src/components/Factory/Sidebar/Resources.tsx b/src/components/Factory/Sidebar/Resources.tsx
new file mode 100644
index 000000000..ece47b9bf
--- /dev/null
+++ b/src/components/Factory/Sidebar/Resources.tsx
@@ -0,0 +1,5 @@
+const Resources = () => {
+ return Gold: 0
;
+};
+
+export default Resources;
diff --git a/src/components/layout/AppMenu.tsx b/src/components/layout/AppMenu.tsx
index 95fe99450..ba67e25e5 100644
--- a/src/components/layout/AppMenu.tsx
+++ b/src/components/layout/AppMenu.tsx
@@ -29,9 +29,20 @@ import { PersonalPreferences } from "../shared/Settings/PersonalPreferences";
const AppMenu = () => {
const requiresAuthorization = isAuthorizationRequired();
const { componentSpec } = useComponentSpec();
- const title = componentSpec?.name;
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const isFactoryGame = window.location.pathname === "/factory";
+
+ const title = isFactoryGame ? "FACTORY" : componentSpec?.name;
+
+ const gameButton = (
+
+
+
+
+
+ );
+
const documentationButton = (
@@ -71,6 +82,7 @@ const AppMenu = () => {
{/* Always visible settings */}
+ {gameButton}
diff --git a/src/routes/Factory/FactoryGame.tsx b/src/routes/Factory/FactoryGame.tsx
new file mode 100644
index 000000000..9df8387de
--- /dev/null
+++ b/src/routes/Factory/FactoryGame.tsx
@@ -0,0 +1,18 @@
+import "@/styles/editor.css";
+
+import { DndContext } from "@dnd-kit/core";
+import { ReactFlowProvider } from "@xyflow/react";
+
+import FactoryGame from "@/components/Factory/FactoryGame";
+
+const Factory = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default Factory;
diff --git a/src/routes/router.ts b/src/routes/router.ts
index 212152935..0704c8948 100644
--- a/src/routes/router.ts
+++ b/src/routes/router.ts
@@ -13,6 +13,7 @@ import { BASE_URL, IS_GITHUB_PAGES } from "@/utils/constants";
import RootLayout from "../components/layout/RootLayout";
import Editor from "./Editor";
+import FactoryGame from "./Factory/FactoryGame";
import Home from "./Home";
import NotFoundPage from "./NotFoundPage";
import PipelineRun from "./PipelineRun";
@@ -36,6 +37,7 @@ export const APP_ROUTES = {
RUNS: RUNS_BASE_PATH,
GITHUB_AUTH_CALLBACK: "/authorize/github",
HUGGINGFACE_AUTH_CALLBACK: "/authorize/huggingface",
+ FACTORY_GAME: "/factory",
};
const rootRoute = createRootRoute({
@@ -96,12 +98,19 @@ const runDetailWithSubgraphRoute = createRoute({
component: PipelineRun,
});
+const factoryGameRoute = createRoute({
+ getParentRoute: () => mainLayout,
+ path: APP_ROUTES.FACTORY_GAME,
+ component: FactoryGame,
+});
+
const appRouteTree = mainLayout.addChildren([
indexRoute,
quickStartRoute,
editorRoute,
runDetailRoute,
runDetailWithSubgraphRoute,
+ factoryGameRoute,
]);
const rootRouteTree = rootRoute.addChildren([