branding v1
This commit is contained in:
@@ -18,6 +18,152 @@ import {
|
||||
Home,
|
||||
} from "lucide-react";
|
||||
|
||||
// Color mappings for navigation items — defined outside component for stable reference
|
||||
const colorClasses = {
|
||||
gray: {
|
||||
inactive: "text-gray-300 hover:text-white hover:bg-gray-800",
|
||||
active: "bg-gray-800 text-white",
|
||||
icon: "text-gray-400",
|
||||
},
|
||||
cyan: {
|
||||
inactive: "text-cyan-300 hover:text-cyan-100 hover:bg-cyan-950/30",
|
||||
active: "bg-cyan-950/50 text-cyan-100 shadow-lg shadow-cyan-900/50",
|
||||
icon: "text-cyan-400",
|
||||
},
|
||||
blue: {
|
||||
inactive: "text-blue-300 hover:text-blue-100 hover:bg-blue-950/30",
|
||||
active: "bg-blue-950/50 text-blue-100 shadow-lg shadow-blue-900/50",
|
||||
icon: "text-blue-400",
|
||||
},
|
||||
violet: {
|
||||
inactive: "text-violet-300 hover:text-violet-100 hover:bg-violet-950/30",
|
||||
active: "bg-violet-950/50 text-violet-100 shadow-lg shadow-violet-900/50",
|
||||
icon: "text-violet-400",
|
||||
},
|
||||
purple: {
|
||||
inactive: "text-purple-300 hover:text-purple-100 hover:bg-purple-950/30",
|
||||
active: "bg-purple-950/50 text-purple-100 shadow-lg shadow-purple-900/50",
|
||||
icon: "text-purple-400",
|
||||
},
|
||||
fuchsia: {
|
||||
inactive: "text-fuchsia-300 hover:text-fuchsia-100 hover:bg-fuchsia-950/30",
|
||||
active:
|
||||
"bg-fuchsia-950/50 text-fuchsia-100 shadow-lg shadow-fuchsia-900/50",
|
||||
icon: "text-fuchsia-400",
|
||||
},
|
||||
rose: {
|
||||
inactive: "text-rose-300 hover:text-rose-100 hover:bg-rose-950/30",
|
||||
active: "bg-rose-950/50 text-rose-100 shadow-lg shadow-rose-900/50",
|
||||
icon: "text-rose-400",
|
||||
},
|
||||
orange: {
|
||||
inactive: "text-orange-300 hover:text-orange-100 hover:bg-orange-950/30",
|
||||
active: "bg-orange-950/50 text-orange-100 shadow-lg shadow-orange-900/50",
|
||||
icon: "text-orange-400",
|
||||
},
|
||||
};
|
||||
|
||||
// Navigation sections with dividers and colors
|
||||
const navSections = [
|
||||
{
|
||||
items: [{ to: "/", label: "Dashboard", icon: Home, color: "gray" }],
|
||||
},
|
||||
{
|
||||
// Component Management - Cool colors (cyan -> blue -> violet)
|
||||
items: [
|
||||
{ to: "/actions", label: "Actions", icon: SquarePlay, color: "cyan" },
|
||||
{ to: "/rules", label: "Rules", icon: SquareArrowRight, color: "blue" },
|
||||
{
|
||||
to: "/triggers",
|
||||
label: "Triggers",
|
||||
icon: SquareDot,
|
||||
color: "violet",
|
||||
},
|
||||
{
|
||||
to: "/sensors",
|
||||
label: "Sensors",
|
||||
icon: SquareAsterisk,
|
||||
color: "purple",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Runtime Logs - Warm colors (fuchsia -> rose -> orange)
|
||||
items: [
|
||||
{
|
||||
to: "/executions",
|
||||
label: "Execution History",
|
||||
icon: CirclePlay,
|
||||
color: "fuchsia",
|
||||
},
|
||||
{
|
||||
to: "/enforcements",
|
||||
label: "Enforcement History",
|
||||
icon: CircleArrowRight,
|
||||
color: "rose",
|
||||
},
|
||||
{
|
||||
to: "/events",
|
||||
label: "Event History",
|
||||
icon: CircleDot,
|
||||
color: "orange",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{ to: "/keys", label: "Keys & Secrets", icon: KeyRound, color: "gray" },
|
||||
{
|
||||
to: "/packs",
|
||||
label: "Pack Management",
|
||||
icon: Package,
|
||||
color: "gray",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// NavLink extracted outside MainLayout so React preserves DOM identity across
|
||||
// re-renders, which is required for CSS transitions to work on collapse/expand.
|
||||
function NavLink({
|
||||
to,
|
||||
label,
|
||||
icon: Icon,
|
||||
color = "gray",
|
||||
isCollapsed,
|
||||
isActive,
|
||||
}: {
|
||||
to: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
color?: string;
|
||||
isCollapsed: boolean;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const colors =
|
||||
colorClasses[color as keyof typeof colorClasses] || colorClasses.gray;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={`flex items-center px-4 py-2 rounded-md transition-colors duration-200 whitespace-nowrap ${
|
||||
isActive ? colors.active : colors.inactive
|
||||
}`}
|
||||
title={isCollapsed ? label : undefined}
|
||||
>
|
||||
<Icon
|
||||
className={`w-5 h-5 flex-shrink-0 ${isActive ? "" : colors.icon}`}
|
||||
/>
|
||||
<span
|
||||
className="ml-3 inline-block overflow-hidden transition-all duration-300"
|
||||
style={{ maxWidth: isCollapsed ? 0 : "10rem" }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MainLayout() {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
@@ -34,184 +180,63 @@ export default function MainLayout() {
|
||||
localStorage.setItem("sidebar-collapsed", isCollapsed.toString());
|
||||
}, [isCollapsed]);
|
||||
|
||||
// Close user menu when expanding sidebar
|
||||
useEffect(() => {
|
||||
if (!isCollapsed) {
|
||||
setShowUserMenu(false);
|
||||
}
|
||||
}, [isCollapsed]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
// Navigation sections with dividers and colors
|
||||
const navSections = [
|
||||
{
|
||||
items: [{ to: "/", label: "Dashboard", icon: Home, color: "gray" }],
|
||||
},
|
||||
{
|
||||
// Component Management - Cool colors (cyan -> blue -> violet)
|
||||
items: [
|
||||
{ to: "/actions", label: "Actions", icon: SquarePlay, color: "cyan" },
|
||||
{ to: "/rules", label: "Rules", icon: SquareArrowRight, color: "blue" },
|
||||
{
|
||||
to: "/triggers",
|
||||
label: "Triggers",
|
||||
icon: SquareDot,
|
||||
color: "violet",
|
||||
},
|
||||
{
|
||||
to: "/sensors",
|
||||
label: "Sensors",
|
||||
icon: SquareAsterisk,
|
||||
color: "purple",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// Runtime Logs - Warm colors (fuchsia -> rose -> orange)
|
||||
items: [
|
||||
{
|
||||
to: "/executions",
|
||||
label: "Execution History",
|
||||
icon: CirclePlay,
|
||||
color: "fuchsia",
|
||||
},
|
||||
{
|
||||
to: "/enforcements",
|
||||
label: "Enforcement History",
|
||||
icon: CircleArrowRight,
|
||||
color: "rose",
|
||||
},
|
||||
{
|
||||
to: "/events",
|
||||
label: "Event History",
|
||||
icon: CircleDot,
|
||||
color: "orange",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{ to: "/keys", label: "Keys & Secrets", icon: KeyRound, color: "gray" },
|
||||
{
|
||||
to: "/packs",
|
||||
label: "Pack Management",
|
||||
icon: Package,
|
||||
color: "gray",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Color mappings for navigation items
|
||||
const colorClasses = {
|
||||
gray: {
|
||||
inactive: "text-gray-300 hover:text-white hover:bg-gray-800",
|
||||
active: "bg-gray-800 text-white",
|
||||
icon: "text-gray-400",
|
||||
},
|
||||
cyan: {
|
||||
inactive: "text-cyan-300 hover:text-cyan-100 hover:bg-cyan-950/30",
|
||||
active: "bg-cyan-950/50 text-cyan-100 shadow-lg shadow-cyan-900/50",
|
||||
icon: "text-cyan-400",
|
||||
},
|
||||
blue: {
|
||||
inactive: "text-blue-300 hover:text-blue-100 hover:bg-blue-950/30",
|
||||
active: "bg-blue-950/50 text-blue-100 shadow-lg shadow-blue-900/50",
|
||||
icon: "text-blue-400",
|
||||
},
|
||||
violet: {
|
||||
inactive: "text-violet-300 hover:text-violet-100 hover:bg-violet-950/30",
|
||||
active: "bg-violet-950/50 text-violet-100 shadow-lg shadow-violet-900/50",
|
||||
icon: "text-violet-400",
|
||||
},
|
||||
purple: {
|
||||
inactive: "text-purple-300 hover:text-purple-100 hover:bg-purple-950/30",
|
||||
active: "bg-purple-950/50 text-purple-100 shadow-lg shadow-purple-900/50",
|
||||
icon: "text-purple-400",
|
||||
},
|
||||
fuchsia: {
|
||||
inactive:
|
||||
"text-fuchsia-300 hover:text-fuchsia-100 hover:bg-fuchsia-950/30",
|
||||
active:
|
||||
"bg-fuchsia-950/50 text-fuchsia-100 shadow-lg shadow-fuchsia-900/50",
|
||||
icon: "text-fuchsia-400",
|
||||
},
|
||||
rose: {
|
||||
inactive: "text-rose-300 hover:text-rose-100 hover:bg-rose-950/30",
|
||||
active: "bg-rose-950/50 text-rose-100 shadow-lg shadow-rose-900/50",
|
||||
icon: "text-rose-400",
|
||||
},
|
||||
orange: {
|
||||
inactive: "text-orange-300 hover:text-orange-100 hover:bg-orange-950/30",
|
||||
active: "bg-orange-950/50 text-orange-100 shadow-lg shadow-orange-900/50",
|
||||
icon: "text-orange-400",
|
||||
},
|
||||
};
|
||||
|
||||
const NavLink = ({
|
||||
to,
|
||||
label,
|
||||
icon: Icon,
|
||||
color = "gray",
|
||||
}: {
|
||||
to: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
color?: string;
|
||||
}) => {
|
||||
const isActive =
|
||||
location.pathname === to ||
|
||||
(to !== "/" && location.pathname.startsWith(to));
|
||||
|
||||
const colors =
|
||||
colorClasses[color as keyof typeof colorClasses] || colorClasses.gray;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
className={`flex items-center gap-3 px-4 py-2 rounded-md transition-all duration-200 ${
|
||||
isActive ? colors.active : colors.inactive
|
||||
} ${isCollapsed ? "justify-center" : ""}`}
|
||||
title={isCollapsed ? label : undefined}
|
||||
>
|
||||
<Icon
|
||||
className={`w-5 h-5 flex-shrink-0 ${isActive ? "" : colors.icon}`}
|
||||
/>
|
||||
{!isCollapsed && <span>{label}</span>}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-gray-100 flex overflow-hidden">
|
||||
<div className="h-full bg-gray-100 flex overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`${
|
||||
isCollapsed ? "w-20" : "w-64"
|
||||
} bg-gray-900 text-white flex flex-col transition-all duration-300 relative flex-shrink-0`}
|
||||
} bg-gray-900 text-white flex flex-col transition-all duration-300 relative flex-shrink-0 overflow-hidden`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-center h-16 bg-gray-800">
|
||||
<Link
|
||||
to="/"
|
||||
className={`font-bold transition-all ${
|
||||
isCollapsed ? "text-lg" : "text-xl"
|
||||
}`}
|
||||
>
|
||||
{isCollapsed ? "A" : "Attune"}
|
||||
<div className="flex items-center justify-center h-20 bg-gray-800 whitespace-nowrap">
|
||||
<Link to="/" className="flex items-center justify-center">
|
||||
<img
|
||||
src="/attune-logo-icon.svg"
|
||||
alt="Attune"
|
||||
className={`h-14 transition-opacity duration-300 ${isCollapsed ? "opacity-100" : "opacity-0 w-0"}`}
|
||||
/>
|
||||
<img
|
||||
src="/attune-logo-navbar.svg"
|
||||
alt="Attune"
|
||||
className={`h-14 transition-opacity duration-300 ${isCollapsed ? "opacity-0 w-0" : "opacity-100"}`}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-4 py-6 overflow-y-auto">
|
||||
<nav className="flex-1 px-4 py-6 overflow-y-auto overflow-x-hidden">
|
||||
{navSections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<div className="space-y-1 mb-3">
|
||||
{section.items.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
color={item.color}
|
||||
/>
|
||||
))}
|
||||
{section.items.map((item) => {
|
||||
const isActive =
|
||||
location.pathname === item.to ||
|
||||
(item.to !== "/" && location.pathname.startsWith(item.to));
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
label={item.label}
|
||||
icon={item.icon}
|
||||
color={item.color}
|
||||
isCollapsed={isCollapsed}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{sectionIndex < navSections.length - 1 && (
|
||||
<div className="my-3 mx-2 border-t border-gray-700" />
|
||||
@@ -221,78 +246,94 @@ export default function MainLayout() {
|
||||
</nav>
|
||||
|
||||
{/* Toggle Button */}
|
||||
<div
|
||||
className={`px-4 py-3 ${isCollapsed ? "flex justify-center" : ""}`}
|
||||
>
|
||||
<div className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="flex items-center gap-2 w-full px-3 py-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-md transition-colors"
|
||||
className="flex items-center w-full px-3 py-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-md transition-colors whitespace-nowrap"
|
||||
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span className="text-sm">Collapse</span>
|
||||
</>
|
||||
)}
|
||||
<div className="w-5 h-5 flex-shrink-0 relative">
|
||||
<ChevronLeft
|
||||
className={`w-5 h-5 absolute inset-0 transition-opacity duration-300 ${
|
||||
isCollapsed ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
/>
|
||||
<ChevronRight
|
||||
className={`w-5 h-5 absolute inset-0 transition-opacity duration-300 ${
|
||||
isCollapsed ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="ml-2 inline-block overflow-hidden text-sm transition-all duration-300"
|
||||
style={{ maxWidth: isCollapsed ? 0 : "10rem" }}
|
||||
>
|
||||
Collapse
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* User Section */}
|
||||
<div className="p-4 bg-gray-800 border-t border-gray-700">
|
||||
{isCollapsed ? (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
className="w-full flex items-center justify-center p-2 rounded-md hover:bg-gray-700 transition-colors"
|
||||
title={user?.login}
|
||||
>
|
||||
<User className="w-6 h-6 text-gray-400" />
|
||||
</button>
|
||||
|
||||
{/* User Menu Popup */}
|
||||
{showUserMenu && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
/>
|
||||
<div className="absolute bottom-full left-0 mb-2 w-48 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-20">
|
||||
<div className="px-4 py-3 border-b border-gray-700">
|
||||
<p className="text-sm font-medium text-white">
|
||||
{user?.login}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 px-4 py-2 text-left text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-gray-800 border-t border-gray-700 overflow-hidden whitespace-nowrap">
|
||||
<div className="relative">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<User className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
||||
<p className="font-medium text-sm truncate">{user?.login}</p>
|
||||
<div className="flex items-center min-w-0">
|
||||
<button
|
||||
onClick={() => isCollapsed && setShowUserMenu(!showUserMenu)}
|
||||
className={`flex-shrink-0 ${isCollapsed ? "cursor-pointer" : "cursor-default"}`}
|
||||
title={user?.login}
|
||||
>
|
||||
<User className="w-5 h-5 text-gray-400" />
|
||||
</button>
|
||||
<span
|
||||
className="ml-2 inline-block overflow-hidden transition-all duration-300 min-w-0"
|
||||
style={{ maxWidth: isCollapsed ? 0 : "8rem" }}
|
||||
>
|
||||
<p className="font-medium text-sm truncate">{user?.login}</p>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-400 hover:text-white p-1 flex-shrink-0"
|
||||
title="Logout"
|
||||
<span
|
||||
className="ml-2 inline-block overflow-hidden transition-all duration-300"
|
||||
style={{ maxWidth: isCollapsed ? 0 : "2rem" }}
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-400 hover:text-white p-1 flex-shrink-0"
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Menu Popup (collapsed mode only) */}
|
||||
{isCollapsed && showUserMenu && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
/>
|
||||
<div className="absolute bottom-full left-0 mb-2 w-48 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-20">
|
||||
<div className="px-4 py-3 border-b border-gray-700">
|
||||
<p className="text-sm font-medium text-white">
|
||||
{user?.login}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 px-4 py-2 text-left text-gray-300 hover:bg-gray-700 hover:text-white transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -200,8 +200,8 @@ function TaskNodeInner({
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
const cur = screenToCanvas(moveEvent.clientX, moveEvent.clientY);
|
||||
onPositionChange(task.id, {
|
||||
x: Math.max(0, cur.x - dragOffset.current.x),
|
||||
y: Math.max(0, cur.y - dragOffset.current.y),
|
||||
x: cur.x - dragOffset.current.x,
|
||||
y: cur.y - dragOffset.current.y,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,25 @@ function gridBackground(pan: { x: number; y: number }, zoom: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a brick-lay tiled watermark using two CSS background layers.
|
||||
* Both layers repeat the same logo at the tile period, but the second
|
||||
* layer is offset by half the period in both axes for a staggered look.
|
||||
* Using background-size equal to the tile period causes the SVG to scale
|
||||
* to fill the tile — the logo's own viewBox whitespace provides the
|
||||
* visual padding around the mark.
|
||||
*/
|
||||
function watermarkBackground(pan: { x: number; y: number }, zoom: number) {
|
||||
const tileW = 1000 * zoom;
|
||||
const tileH = 700 * zoom;
|
||||
const logo = `url("/attune-logo-watermark-tile.svg")`;
|
||||
return {
|
||||
backgroundImage: `${logo}, ${logo}`,
|
||||
backgroundSize: `${tileW}px ${tileH}px, ${tileW}px ${tileH}px`,
|
||||
backgroundPosition: `${pan.x}px ${pan.y}px, ${pan.x + tileW / 2}px ${pan.y + tileH / 2}px`,
|
||||
};
|
||||
}
|
||||
|
||||
export type ScreenToCanvas = (
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
@@ -79,6 +98,7 @@ export default function WorkflowCanvas({
|
||||
}: WorkflowCanvasProps) {
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const watermarkRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// ---- Camera state ----
|
||||
// We keep refs for high-frequency updates (panning/zooming) and sync to
|
||||
@@ -141,6 +161,12 @@ export default function WorkflowCanvas({
|
||||
canvasRef.current.style.backgroundSize = bg.backgroundSize;
|
||||
canvasRef.current.style.backgroundPosition = bg.backgroundPosition;
|
||||
}
|
||||
if (watermarkRef.current) {
|
||||
const wm = watermarkBackground(panRef.current, zoomRef.current);
|
||||
watermarkRef.current.style.backgroundImage = wm.backgroundImage;
|
||||
watermarkRef.current.style.backgroundSize = wm.backgroundSize;
|
||||
watermarkRef.current.style.backgroundPosition = wm.backgroundPosition;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ---- Canvas click (deselect / cancel connection) ----
|
||||
@@ -461,17 +487,22 @@ export default function WorkflowCanvas({
|
||||
|
||||
// ---- Inner div dimensions (large enough to contain all content) ----
|
||||
const innerSize = useMemo(() => {
|
||||
let minX = 0;
|
||||
let minY = 0;
|
||||
let maxX = 4000;
|
||||
let maxY = 4000;
|
||||
for (const task of tasks) {
|
||||
minX = Math.min(minX, task.position.x - 100);
|
||||
minY = Math.min(minY, task.position.y - 100);
|
||||
maxX = Math.max(maxX, task.position.x + 500);
|
||||
maxY = Math.max(maxY, task.position.y + 500);
|
||||
}
|
||||
return { width: maxX, height: maxY };
|
||||
return { width: maxX - minX, height: maxY - minY };
|
||||
}, [tasks]);
|
||||
|
||||
// ---- Grid background (recomputed from React state for the render) ----
|
||||
const gridBg = useMemo(() => gridBackground(pan, zoom), [pan, zoom]);
|
||||
const wmBg = useMemo(() => watermarkBackground(pan, zoom), [pan, zoom]);
|
||||
|
||||
// Zoom percentage for display
|
||||
const zoomPercent = Math.round(zoom * 100);
|
||||
@@ -491,6 +522,17 @@ export default function WorkflowCanvas({
|
||||
onMouseUp={handleCanvasMouseUp}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{/* Tiled watermark layer — moves with grid, transparent */}
|
||||
<div
|
||||
ref={watermarkRef}
|
||||
className="absolute inset-0 pointer-events-none opacity-[0.15]"
|
||||
style={{
|
||||
backgroundImage: wmBg.backgroundImage,
|
||||
backgroundSize: wmBg.backgroundSize,
|
||||
backgroundPosition: wmBg.backgroundPosition,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Transformed canvas content */}
|
||||
<div
|
||||
ref={innerRef}
|
||||
|
||||
@@ -525,15 +525,19 @@ function WorkflowEdgesInner({
|
||||
|
||||
const svgBounds = useMemo(() => {
|
||||
if (tasks.length === 0) return { width: 2000, height: 2000 };
|
||||
let minX = 0;
|
||||
let minY = 0;
|
||||
let maxX = 0;
|
||||
let maxY = 0;
|
||||
for (const task of tasks) {
|
||||
minX = Math.min(minX, task.position.x - 100);
|
||||
minY = Math.min(minY, task.position.y - 100);
|
||||
maxX = Math.max(maxX, task.position.x + nodeWidth + 100);
|
||||
maxY = Math.max(maxY, task.position.y + nodeHeight + 100);
|
||||
}
|
||||
return {
|
||||
width: Math.max(maxX, 2000),
|
||||
height: Math.max(maxY, 2000),
|
||||
width: Math.max(maxX - minX, 2000),
|
||||
height: Math.max(maxY - minY, 2000),
|
||||
};
|
||||
}, [tasks, nodeWidth, nodeHeight]);
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
@apply h-full;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flash-highlight {
|
||||
0% {
|
||||
background-color: rgb(191 219 254); /* blue-200 */
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function ActionsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full">
|
||||
{/* Left sidebar - Actions List */}
|
||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||
|
||||
@@ -608,14 +608,14 @@ export default function WorkflowBuilderPage() {
|
||||
|
||||
if (isEditing && workflowLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-4rem)] flex flex-col overflow-hidden">
|
||||
<div className="h-full flex flex-col overflow-hidden">
|
||||
{/* Top toolbar */}
|
||||
<div className="flex-shrink-0 bg-white border-b border-gray-200 px-4 py-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -534,7 +534,7 @@ export default function ExecutionsPage() {
|
||||
}, [filteredExecutions]);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full">
|
||||
{/* Main content area */}
|
||||
<div
|
||||
className={`flex-1 min-w-0 overflow-y-auto p-6 ${selectedExecutionId ? "mr-0" : ""}`}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function PacksPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full">
|
||||
{/* Left sidebar - Packs List */}
|
||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function RulesPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full">
|
||||
{/* Left sidebar - Rules List */}
|
||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function SensorsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full">
|
||||
{/* Left sidebar - Sensors List */}
|
||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function TriggersPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)]">
|
||||
<div className="flex h-full">
|
||||
{/* Left sidebar - Triggers List */}
|
||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||
|
||||
Reference in New Issue
Block a user