-
- {/* User Menu Popup */}
- {showUserMenu && (
- <>
-
+
-
-
-
{user?.login}
+
+
+
+ {user?.login}
+
-
+
+
- )}
+
+ {/* User Menu Popup (collapsed mode only) */}
+ {isCollapsed && showUserMenu && (
+ <>
+
setShowUserMenu(false)}
+ />
+
+ >
+ )}
+
+
+ {/* Main Content */}
diff --git a/web/src/components/workflows/TaskNode.tsx b/web/src/components/workflows/TaskNode.tsx
index f1a4483..f62d444 100644
--- a/web/src/components/workflows/TaskNode.tsx
+++ b/web/src/components/workflows/TaskNode.tsx
@@ -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,
});
};
diff --git a/web/src/components/workflows/WorkflowCanvas.tsx b/web/src/components/workflows/WorkflowCanvas.tsx
index 56a487f..2ff1b47 100644
--- a/web/src/components/workflows/WorkflowCanvas.tsx
+++ b/web/src/components/workflows/WorkflowCanvas.tsx
@@ -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
(null);
const innerRef = useRef(null);
+ const watermarkRef = useRef(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 */}
+
+
{/* Transformed canvas content */}
{
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]);
diff --git a/web/src/index.css b/web/src/index.css
index c32a393..62b59d2 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -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 */
diff --git a/web/src/pages/actions/ActionsPage.tsx b/web/src/pages/actions/ActionsPage.tsx
index 44a5ad0..83b6abc 100644
--- a/web/src/pages/actions/ActionsPage.tsx
+++ b/web/src/pages/actions/ActionsPage.tsx
@@ -85,7 +85,7 @@ export default function ActionsPage() {
}
return (
-
+
{/* Left sidebar - Actions List */}
diff --git a/web/src/pages/actions/WorkflowBuilderPage.tsx b/web/src/pages/actions/WorkflowBuilderPage.tsx
index b5f2fa9..ccb1946 100644
--- a/web/src/pages/actions/WorkflowBuilderPage.tsx
+++ b/web/src/pages/actions/WorkflowBuilderPage.tsx
@@ -608,14 +608,14 @@ export default function WorkflowBuilderPage() {
if (isEditing && workflowLoading) {
return (
-
+
);
}
return (
-
+
{/* Top toolbar */}
diff --git a/web/src/pages/executions/ExecutionsPage.tsx b/web/src/pages/executions/ExecutionsPage.tsx
index beec2bf..6c8a102 100644
--- a/web/src/pages/executions/ExecutionsPage.tsx
+++ b/web/src/pages/executions/ExecutionsPage.tsx
@@ -534,7 +534,7 @@ export default function ExecutionsPage() {
}, [filteredExecutions]);
return (
-
+
{/* Main content area */}
+
{/* Left sidebar - Packs List */}
diff --git a/web/src/pages/rules/RulesPage.tsx b/web/src/pages/rules/RulesPage.tsx
index 0cd31f1..6ecb41d 100644
--- a/web/src/pages/rules/RulesPage.tsx
+++ b/web/src/pages/rules/RulesPage.tsx
@@ -87,7 +87,7 @@ export default function RulesPage() {
}
return (
-
+
{/* Left sidebar - Rules List */}
diff --git a/web/src/pages/sensors/SensorsPage.tsx b/web/src/pages/sensors/SensorsPage.tsx
index 6ed50df..cde008b 100644
--- a/web/src/pages/sensors/SensorsPage.tsx
+++ b/web/src/pages/sensors/SensorsPage.tsx
@@ -73,7 +73,7 @@ export default function SensorsPage() {
}
return (
-
+
{/* Left sidebar - Sensors List */}
diff --git a/web/src/pages/triggers/TriggersPage.tsx b/web/src/pages/triggers/TriggersPage.tsx
index 77bf675..3748361 100644
--- a/web/src/pages/triggers/TriggersPage.tsx
+++ b/web/src/pages/triggers/TriggersPage.tsx
@@ -90,7 +90,7 @@ export default function TriggersPage() {
}
return (
-
+
{/* Left sidebar - Triggers List */}