diff --git a/app_modules_overview.xlsx b/app_modules_overview.xlsx
new file mode 100644
index 0000000..e62f247
Binary files /dev/null and b/app_modules_overview.xlsx differ
diff --git a/src/app/(modules)/visual-copilot/page.tsx b/src/app/(modules)/visual-copilot/page.tsx
new file mode 100644
index 0000000..2a675a9
--- /dev/null
+++ b/src/app/(modules)/visual-copilot/page.tsx
@@ -0,0 +1,5 @@
+import { VisualCopilotModule } from "@/modules/visual-copilot";
+
+export default function VisualCopilotPage() {
+ return ;
+}
diff --git a/src/config/flags.ts b/src/config/flags.ts
index 4ae16fd..faf580c 100644
--- a/src/config/flags.ts
+++ b/src/config/flags.ts
@@ -98,6 +98,14 @@ export const DEFAULT_FLAGS: FeatureFlag[] = [
category: "module",
overridable: true,
},
+ {
+ key: "module.visual-copilot",
+ enabled: true,
+ label: "Visual CoPilot",
+ description: "Canvas AI pentru vizualizare arhitecturală",
+ category: "module",
+ overridable: true,
+ },
{
key: "module.hot-desk",
enabled: true,
diff --git a/src/config/modules.ts b/src/config/modules.ts
index 9ec3a4c..e9cd504 100644
--- a/src/config/modules.ts
+++ b/src/config/modules.ts
@@ -14,6 +14,7 @@ import { tagManagerConfig } from "@/modules/tag-manager/config";
import { miniUtilitiesConfig } from "@/modules/mini-utilities/config";
import { aiChatConfig } from "@/modules/ai-chat/config";
import { hotDeskConfig } from "@/modules/hot-desk/config";
+import { visualCopilotConfig } from "@/modules/visual-copilot/config";
/**
* Toate configurările modulelor ArchiTools, ordonate după navOrder.
@@ -31,8 +32,9 @@ export const MODULE_CONFIGS: ModuleConfig[] = [
hotDeskConfig, // navOrder: 33 | management
tagManagerConfig, // navOrder: 40 | tools
miniUtilitiesConfig, // navOrder: 41 | tools
- promptGeneratorConfig, // navOrder: 50 | ai
- aiChatConfig, // navOrder: 51 | ai
+ promptGeneratorConfig, // navOrder: 50 | ai
+ aiChatConfig, // navOrder: 51 | ai
+ visualCopilotConfig, // navOrder: 52 | ai
];
// Înregistrare automată a tuturor modulelor în registru
diff --git a/src/modules/visual-copilot/components/visual-copilot-module.tsx b/src/modules/visual-copilot/components/visual-copilot-module.tsx
new file mode 100644
index 0000000..68f5d3c
--- /dev/null
+++ b/src/modules/visual-copilot/components/visual-copilot-module.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { ExternalLink, AlertTriangle, Maximize2 } from "lucide-react";
+import { useState } from "react";
+
+const VIM_URL = process.env.NEXT_PUBLIC_VIM_URL ?? "";
+
+export function VisualCopilotModule() {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ if (!VIM_URL) {
+ return (
+
+
+
+
+ Visual CoPilot nu este configurat
+
+
+ Setează{" "}
+
+ NEXT_PUBLIC_VIM_URL
+ {" "}
+ în fișierul .env
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Floating action bar */}
+
+
+
+
+ Tab nou
+
+
+
+ );
+}
diff --git a/src/modules/visual-copilot/config.ts b/src/modules/visual-copilot/config.ts
new file mode 100644
index 0000000..d45bdb9
--- /dev/null
+++ b/src/modules/visual-copilot/config.ts
@@ -0,0 +1,18 @@
+import type { ModuleConfig } from "@/core/module-registry/types";
+
+export const visualCopilotConfig: ModuleConfig = {
+ id: "visual-copilot",
+ name: "Visual CoPilot",
+ description:
+ "Canvas AI pentru vizualizare arhitecturală — generare imagini cu noduri conectate",
+ icon: "image",
+ route: "/visual-copilot",
+ category: "ai",
+ featureFlag: "module.visual-copilot",
+ visibility: "all",
+ version: "0.1.0",
+ dependencies: [],
+ storageNamespace: "visual-copilot",
+ navOrder: 52,
+ tags: ["ai", "imagine", "canvas", "arhitectura", "generare", "flux"],
+};
diff --git a/src/modules/visual-copilot/index.ts b/src/modules/visual-copilot/index.ts
new file mode 100644
index 0000000..255ed06
--- /dev/null
+++ b/src/modules/visual-copilot/index.ts
@@ -0,0 +1,2 @@
+export { VisualCopilotModule } from "./components/visual-copilot-module";
+export { visualCopilotConfig } from "./config";
diff --git a/src/shared/components/layout/app-shell.tsx b/src/shared/components/layout/app-shell.tsx
index e8feb12..9b36204 100644
--- a/src/shared/components/layout/app-shell.tsx
+++ b/src/shared/components/layout/app-shell.tsx
@@ -1,35 +1,69 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect, useCallback } from 'react';
+import { usePathname } from 'next/navigation';
import { Sidebar } from './sidebar';
import { Header } from './header';
import { Sheet, SheetContent } from '@/shared/components/ui/sheet';
+import { cn } from '@/shared/lib/utils';
+
+const SIDEBAR_COLLAPSED_KEY = 'sidebar:collapsed';
+
+// Routes that need full viewport height and no padding (canvas tools)
+const FULLSCREEN_ROUTES = ['/visual-copilot'];
interface AppShellProps {
children: React.ReactNode;
}
export function AppShell({ children }: AppShellProps) {
- const [sidebarOpen, setSidebarOpen] = useState(false);
+ const pathname = usePathname();
+ const [mobileOpen, setMobileOpen] = useState(false);
+ const [collapsed, setCollapsed] = useState(false);
+ const [mounted, setMounted] = useState(false);
+
+ // Read persisted state after mount (avoids SSR mismatch)
+ useEffect(() => {
+ const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY);
+ if (stored === 'true') setCollapsed(true);
+ setMounted(true);
+ }, []);
+
+ const handleToggle = useCallback(() => {
+ setCollapsed((prev) => {
+ const next = !prev;
+ localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(next));
+ return next;
+ });
+ }, []);
+
+ const isFullscreen = FULLSCREEN_ROUTES.some((r) => pathname.startsWith(r));
return (
{/* Desktop sidebar */}
-
+ {mounted && (
+
+ )}
{/* Mobile sidebar */}
-
+
-
+ setMobileOpen(false)} />
{/* Main area */}
- setSidebarOpen(true)} />
-
+ setMobileOpen(true)} />
+
{children}
diff --git a/src/shared/components/layout/sidebar.tsx b/src/shared/components/layout/sidebar.tsx
index 9830b7c..49f1473 100644
--- a/src/shared/components/layout/sidebar.tsx
+++ b/src/shared/components/layout/sidebar.tsx
@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation";
import { useMemo, useState, useCallback, useEffect } from "react";
import { useTheme } from "next-themes";
import * as Icons from "lucide-react";
+import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { buildNavigation } from "@/config/navigation";
import { COMPANIES } from "@/config/companies";
import { useFeatureFlag } from "@/core/feature-flags";
@@ -13,6 +14,14 @@ import { useAuth } from "@/core/auth/auth-provider";
import { cn } from "@/shared/lib/utils";
import { ScrollArea } from "@/shared/components/ui/scroll-area";
import { Separator } from "@/shared/components/ui/separator";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/shared/components/ui/tooltip";
+
+const SIDEBAR_COLLAPSED_KEY = "sidebar:collapsed";
function DynamicIcon({
name,
@@ -37,6 +46,7 @@ function DynamicIcon({
function NavItem({
item,
isActive,
+ collapsed,
}: {
item: {
id: string;
@@ -46,20 +56,36 @@ function NavItem({
featureFlag: string;
};
isActive: boolean;
+ collapsed: boolean;
}) {
const enabled = useFeatureFlag(item.featureFlag);
if (!enabled) return null;
+ const linkClass = cn(
+ "flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
+ collapsed && "justify-center px-2",
+ isActive
+ ? "bg-accent text-accent-foreground font-medium"
+ : "text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground",
+ );
+
+ if (collapsed) {
+ return (
+
+
+
+
+
+
+
+ {item.label}
+
+
+ );
+ }
+
return (
-
+
{item.label}
@@ -80,7 +106,6 @@ const COMPANY_GLOW: Record = {
"studii-de-teren": "hover:drop-shadow-[0_0_6px_#f59e0b]",
};
-// Secret combo: click logos in order BTG → US → SDT to trigger confetti
const SECRET_COMBO = ["beletage", "urban-switch", "studii-de-teren"];
function SidebarLogos() {
@@ -94,14 +119,12 @@ function SidebarLogos() {
const isDark = mounted ? resolvedTheme === "dark" : false;
- // Reset combo after 3 seconds of inactivity
useEffect(() => {
if (comboProgress === 0) return;
const timer = setTimeout(() => setComboProgress(0), 3000);
return () => clearTimeout(timer);
}, [comboProgress]);
- // Hide confetti after 2 seconds
useEffect(() => {
if (!showConfetti) return;
const timer = setTimeout(() => setShowConfetti(false), 2000);
@@ -113,11 +136,9 @@ function SidebarLogos() {
e.preventDefault();
e.stopPropagation();
- // Trigger individual animation
setAnimatingId(companyId);
setTimeout(() => setAnimatingId(null), 500);
- // Check secret combo
if (SECRET_COMBO[comboProgress] === companyId) {
const next = comboProgress + 1;
if (next >= SECRET_COMBO.length) {
@@ -160,7 +181,6 @@ function SidebarLogos() {
)}
title={company.shortName}
>
- {/* Light variant — hidden in dark mode */}
- {/* Dark variant — hidden in light mode */}
@@ -211,7 +229,12 @@ function SidebarLogos() {
);
}
-export function Sidebar() {
+interface SidebarProps {
+ collapsed: boolean;
+ onToggle: () => void;
+}
+
+export function Sidebar({ collapsed, onToggle }: SidebarProps) {
const pathname = usePathname();
const navGroups = useMemo(() => buildNavigation(), []);
const { user } = useAuth();
@@ -224,55 +247,124 @@ export function Sidebar() {
}, [user?.name]);
return (
-
+
);
}