diff --git a/src/assets/main.css b/src/assets/main.css index 830aacc..592d9f1 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -6,6 +6,8 @@ --mmt-tooltip-zindex: 10; --datagrid-drag-drop-zindex: 5; --datagrid-resize-zindex: 1; + --color-splitter: color-mix(in oklab, var(--color-base-content) 50%, #0000); + --color-splitter-active: color-mix(in oklab, var(--color-base-content) 50%, #ffff); } .mmt-tooltip-container { diff --git a/src/components/tabs/assets/tabs.js b/src/components/tabs/assets/tabs.js index 526b194..fff1235 100644 --- a/src/components/tabs/assets/tabs.js +++ b/src/components/tabs/assets/tabs.js @@ -1,6 +1,5 @@ function getTabContentBoundaries(tabsId) { const tabsContainer = document.getElementById(tabsId) - console.debug("tabsContainer", tabsContainer) const contentDiv = tabsContainer.querySelector('.mmt-tabs-content') const boundaries = contentDiv.getBoundingClientRect() diff --git a/src/components/workflows/Readme.md b/src/components/workflows/Readme.md new file mode 100644 index 0000000..83af068 --- /dev/null +++ b/src/components/workflows/Readme.md @@ -0,0 +1,12 @@ +# id + +**Workflow Designer ids**: + +using `_id={WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}{get_unique_id()}` + +| Name | value | +|------------|----------------| +| Canvas | `c_{self._id}` | +| Designer | `d_{self._id}` | +| Properties | `p_{self._id}` | +| Spliter | `s_{self._id}` | diff --git a/src/components/workflows/WorkflowsApp.py b/src/components/workflows/WorkflowsApp.py index 1a63b6e..53648c5 100644 --- a/src/components/workflows/WorkflowsApp.py +++ b/src/components/workflows/WorkflowsApp.py @@ -61,4 +61,11 @@ def post(session, _id: str, from_id: str, to_id: str): logger.debug( f"Entering {Routes.AddConnection} with args {debug_session(session)}, {_id=}, {from_id=}, {to_id=}") instance = InstanceManager.get(session, _id) - return instance.add_connection(from_id, to_id) \ No newline at end of file + return instance.add_connection(from_id, to_id) + +@rt(Routes.ResizeDesigner) +def post(session, _id: str, designer_height: int): + logger.debug( + f"Entering {Routes.ResizeDesigner} with args {debug_session(session)}, {_id=}, {designer_height=}") + instance = InstanceManager.get(session, _id) + return instance.set_designer_height(designer_height) \ No newline at end of file diff --git a/src/components/workflows/assets/Workflows.css b/src/components/workflows/assets/Workflows.css index b3fb045..bff75e4 100644 --- a/src/components/workflows/assets/Workflows.css +++ b/src/components/workflows/assets/Workflows.css @@ -7,15 +7,67 @@ cursor: grabbing; } +.wkf-splitter { + cursor: row-resize; + height: 1px; + background-color: var(--color-splitter); + margin: 4px 0; + transition: background-color 0.2s; + position: relative; /* Ensure the parent has position relative */ + +} + +.wkf-splitter::after { + --color-resize: var(--color-splitter); + content: ''; /* This is required */ + position: absolute; /* Position as needed */ + z-index: 1; + display: block; /* Makes it a block element */ + height: 6px; + width: 20px; + background-color: var(--color-splitter); + + /* Center horizontally */ + left: 50%; + transform: translateX(-50%); + + /* Center vertically */ + top: 50%; + margin-top: -3px; /* Half of the height */ + /* Alternatively: transform: translate(-50%, -50%); */ +} + + +.wkf-splitter:hover, .wkf-splitter-active { + background-color: var(--color-splitter-active); +} + +.wkf-designer { + min-height: 230px; +} + +.wkf-properties { + box-sizing: border-box; +} + .wkf-canvas { position: relative; - min-height: 230px; background-image: linear-gradient(rgba(0,0,0,.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,.1) 1px, transparent 1px); background-size: 20px 20px; } +.wkf-toolbox { + min-height: 230px; + width: 8rem; /* w-32 (32 * 0.25rem = 8rem) */ + padding: 0.5rem; /* p-2 */ + background-color: var(--color-base-100); /* bg-base-100 */ + border-radius: 0.5rem; /* rounded-lg */ + border-width: 1px; /* border */ +} + + .wkf-workflow-component { position: absolute; cursor: move; @@ -66,7 +118,6 @@ background: #ef4444; } - .wkf-output-point { right: -6px; top: 50%; @@ -88,3 +139,4 @@ 70% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0); } 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } } + diff --git a/src/components/workflows/assets/Workflows.js b/src/components/workflows/assets/Workflows.js index 063c1df..550692c 100644 --- a/src/components/workflows/assets/Workflows.js +++ b/src/components/workflows/assets/Workflows.js @@ -1,4 +1,9 @@ function bindWorkflowDesigner(elementId) { + bindWorkflowDesignerToolbox(elementId) + bindWorkflowDesignerSplitter(elementId) +} + +function bindWorkflowDesignerToolbox(elementId) { // Store state for this specific designer instance const designer = { draggedType: null, @@ -209,7 +214,9 @@ function bindWorkflowDesigner(elementId) { else { const component = event.target.closest('.wkf-workflow-component'); - componentId = component ? component.dataset.componentId : null + if (!component) return; + + componentId = component.dataset.componentId htmx.ajax('POST', '/workflows/select-connection', { target: `#c_${elementId}`, headers: {"Content-Type": "application/x-www-form-urlencoded"}, @@ -301,6 +308,97 @@ function bindWorkflowDesigner(elementId) { return designer; } +/** + * Binds drag resize functionality to a workflow designer splitter + * @param {string} elementId - The base ID of the workflow designer element + */ +function bindWorkflowDesignerSplitter(elementId) { + // Get the elements + const designer = document.getElementById(`d_${elementId}`); + const splitter = document.getElementById(`s_${elementId}`); + const properties = document.getElementById(`p_${elementId}`); + const designerMinHeight = parseInt(designer.style.minHeight, 10) || 230; + + if (!designer || !splitter) { + console.error("Cannot find all required elements for workflow designer splitter"); + return; + } + + // Initialize drag state + let isResizing = false; + let startY = 0; + let startDesignerHeight = 0; + + // Mouse down event - start dragging + splitter.addEventListener('mousedown', (e) => { + e.preventDefault(); + isResizing = true; + startY = e.clientY; + startDesignerHeight = parseInt(designer.style.height, 10) || designer.parentNode.getBoundingClientRect().height; + console.debug("startDesignerHeight", startDesignerHeight); + + document.body.style.userSelect = 'none'; // Disable text selection + document.body.style.cursor = "row-resize"; // Change cursor style globally for horizontal splitter + splitter.classList.add('wkf-splitter-active'); // Add class for visual feedback + }); + + // Mouse move event - update heights while dragging + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + // Calculate new height + const deltaY = e.clientY - startY; + const newDesignerHeight = Math.max(designerMinHeight, startDesignerHeight + deltaY); // Enforce minimum height + designer.style.height = `${newDesignerHeight}px`; + + // Update properties panel height if it exists + if (properties) { + const containerHeight = designer.parentNode.getBoundingClientRect().height; + const propertiesHeight = Math.max(50, containerHeight - newDesignerHeight - splitter.offsetHeight); + properties.style.height = `${propertiesHeight}px`; + } + + console.debug("newDesignerHeight", newDesignerHeight); + }); + + // Mouse up event - stop dragging + document.addEventListener('mouseup', () => { + if (!isResizing) return; + + isResizing = false; + document.body.style.cursor = ""; // Reset cursor + document.body.style.userSelect = ""; // Re-enable text selection + splitter.classList.remove('wkf-splitter-active'); + + // Store the current state + const designerHeight = parseInt(designer.style.height, 10); + saveDesignerHeight(elementId, designerHeight); + }); + + // Handle case when mouse leaves the window + document.addEventListener('mouseleave', () => { + if (isResizing) { + isResizing = false; + document.body.style.cursor = ""; // Reset cursor + document.body.style.userSelect = ""; // Re-enable text selection + splitter.classList.remove('wkf-splitter-active'); + } + }); + + // Function to save the designer height + function saveDesignerHeight(id, height) { + htmx.ajax('POST', '/workflows/resize-designer', { + target: `#${elementId}`, + headers: {"Content-Type": "application/x-www-form-urlencoded"}, + swap: "outerHTML", + values: { + _id: elementId, + designer_height: height, + } + }); + } +} + function _isOverlapping(rect, circle) { // Find the closest point on the rectangle to the circle's center const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width)); @@ -313,4 +411,4 @@ function _isOverlapping(rect, circle) { // Check if the distance is less than or equal to the circle's radius return distanceSquared <= circle.radius * circle.radius; -} +} \ No newline at end of file diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py index b84ca3f..7dedeb4 100644 --- a/src/components/workflows/components/WorkflowDesigner.py +++ b/src/components/workflows/components/WorkflowDesigner.py @@ -108,11 +108,17 @@ class WorkflowDesigner(BaseComponent): return self.refresh() + def set_designer_height(self, height): + self._state.designer_height = height + self._db.save_state(self._key, self._state) + return self.__ft__() # refresh the whole component + def __ft__(self): return Div( H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"), P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm mb-6"), self._mk_designer(), + Div(cls="wkf-splitter", id=f"s_{self._id}"), self._mk_properties(), Script(f"bindWorkflowDesigner('{self._id}');"), **apply_boundaries(self._boundaries), @@ -205,14 +211,12 @@ class WorkflowDesigner(BaseComponent): # Render components *[self._mk_workflow_component(comp) for comp in self._state.components.values()], - - cls="wkf-canvas bg-base-100 rounded-lg border relative", ), def _mk_canvas(self, oob=False): return Div( self._mk_elements(), - cls="flex-1", + cls="wkf-canvas flex-1 rounded-lg border flex-1", id=f"c_{self._id}", hx_swap_oob='true' if oob else None, ), @@ -222,9 +226,9 @@ class WorkflowDesigner(BaseComponent): Div( *[self._mk_toolbox_item(comp_type, info) for comp_type, info in COMPONENT_TYPES.items()], - cls="space-y-1" + # cls="space-y-1" ), - cls="w-32 p-2 bg-base-100 rounded-lg border" + cls="wkf-toolbox" ) def _mk_designer(self): @@ -232,9 +236,9 @@ class WorkflowDesigner(BaseComponent): self._mk_toolbox(), # (Left side) self._mk_canvas(), # (Right side) - cls="flex gap-4", + cls="wkf-designer flex gap-4", id=f"d_{self._id}", - style=f"max-height:{self._state.designer_height}px;" + style=f"height:{self._state.designer_height}px;" ) def _mk_properties(self): @@ -248,10 +252,11 @@ class WorkflowDesigner(BaseComponent): ), ), - cls="p-2 bg-base-100 rounded-lg border mt-4", - style=f"max-height:{self._get_properties_height()}px;", + cls="p-2 bg-base-100 rounded-lg border", + style=f"height:{self._get_properties_height()}px;", id=f"p_{self._id}", ) def _get_properties_height(self): - return self._boundaries["height"] - self._state.designer_height + print(f"height: {self._boundaries['height']}") + return self._boundaries["height"] - self._state.designer_height - 86 diff --git a/src/components/workflows/constants.py b/src/components/workflows/constants.py index 3986a95..f8f2f70 100644 --- a/src/components/workflows/constants.py +++ b/src/components/workflows/constants.py @@ -17,3 +17,4 @@ class Routes: MoveComponent = "/move-component" DeleteComponent = "/delete-component" AddConnection = "/add-connection" + ResizeDesigner = "/resize-designer" \ No newline at end of file