refactor(ui): styling for form edit mode (wip)

This commit is contained in:
psychedelicious
2025-02-25 13:08:56 +10:00
parent 9d9b2f73db
commit 7591adebd5
4 changed files with 155 additions and 50 deletions

View File

@@ -1,5 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Text } from '@invoke-ai/ui-library';
import { Box, Flex, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import {
ContainerContextProvider,
@@ -7,7 +7,7 @@ import {
useDepthContext,
} from 'features/nodes/components/sidePanel/builder/contexts';
import { DividerElementComponent } from 'features/nodes/components/sidePanel/builder/DividerElementComponent';
import { useIsRootElement } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
import { useIsRootElement, useRootElementDropTarget } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { HeadingElementComponent } from 'features/nodes/components/sidePanel/builder/HeadingElementComponent';
import { NodeFieldElementComponent } from 'features/nodes/components/sidePanel/builder/NodeFieldElementComponent';
@@ -22,33 +22,28 @@ import {
isNodeFieldElement,
isTextElement,
} from 'features/nodes/types/workflow';
import { memo } from 'react';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { Equals } from 'tsafe';
import { assert } from 'tsafe';
const sx: SystemStyleObject = {
gap: 4,
flex: '1 1 0',
'&[data-depth="0"]': {
flex: 1,
},
'&[data-container-layout="column"]': {
flexDir: 'column',
},
'&[data-container-layout="row"]': {
flexDir: 'row',
},
};
const ContainerElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
const mode = useAppSelector(selectWorkflowMode);
const isRootElement = useIsRootElement(id);
if (!el || !isContainerElement(el)) {
return null;
}
if (isRootElement && mode === 'view') {
return <RootContainerElementComponentViewMode el={el} />;
}
if (isRootElement && mode === 'edit') {
return <RootContainerElementComponentEditMode el={el} />;
}
if (mode === 'view') {
return <ContainerElementComponentViewMode el={el} />;
}
@@ -58,6 +53,90 @@ const ContainerElementComponent = memo(({ id }: { id: string }) => {
});
ContainerElementComponent.displayName = 'ContainerElementComponent';
const rootViewModeSx: SystemStyleObject = {
position: 'relative',
alignItems: 'center',
borderRadius: 'base',
w: 'full',
h: 'full',
gap: 4,
display: 'flex',
flex: 1,
'&[data-container-layout="column"]': {
flexDir: 'column',
alignItems: 'flex-start',
},
'&[data-container-layout="row"]': {
flexDir: 'row',
},
};
const RootContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
const { id, data } = el;
const { children, layout } = data;
return (
<DepthContextProvider depth={0}>
<ContainerContextProvider id={id} layout={layout}>
<Box id={id} className={CONTAINER_CLASS_NAME} sx={rootViewModeSx} data-container-layout={layout} data-depth={0}>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
</Box>
</ContainerContextProvider>
</DepthContextProvider>
);
});
RootContainerElementComponentViewMode.displayName = 'RootContainerElementComponentViewMode';
const rootEditModeSx: SystemStyleObject = {
...rootViewModeSx,
'&[data-is-dragging-over="true"]': {
opacity: 1,
bg: 'base.850',
},
};
const RootContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }) => {
const { id, data } = el;
const { children, layout } = data;
const ref = useRef<HTMLDivElement>(null);
const isDraggingOver = useRootElementDropTarget(ref);
return (
<DepthContextProvider depth={0}>
<ContainerContextProvider id={id} layout={layout}>
<Flex
ref={ref}
id={id}
className={CONTAINER_CLASS_NAME}
sx={rootEditModeSx}
data-container-layout={layout}
data-depth={0}
data-is-dragging-over={isDraggingOver}
>
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
{children.length === 0 && <RootPlaceholder />}
</Flex>
</ContainerContextProvider>
</DepthContextProvider>
);
});
RootContainerElementComponentEditMode.displayName = 'RootContainerElementComponentEditMode';
const sx: SystemStyleObject = {
gap: 4,
flex: '1 1 0',
'&[data-container-layout="column"]': {
flexDir: 'column',
},
'&[data-container-layout="row"]': {
flexDir: 'row',
},
};
const ContainerElementComponentViewMode = memo(({ el }: { el: ContainerElement }) => {
const { t } = useTranslation();
const depth = useDepthContext();
@@ -87,7 +166,6 @@ const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }
const depth = useDepthContext();
const { id, data } = el;
const { children, layout } = data;
const isRootElement = useIsRootElement(id);
return (
<FormElementEditModeWrapper element={el}>
@@ -97,8 +175,7 @@ const ContainerElementComponentEditMode = memo(({ el }: { el: ContainerElement }
{children.map((childId) => (
<FormElementComponent key={childId} id={childId} />
))}
{children.length === 0 && isRootElement && <RootPlaceholder />}
{children.length === 0 && !isRootElement && <NonRootPlaceholder />}
{children.length === 0 && <NonRootPlaceholder />}
</Flex>
</ContainerContextProvider>
</DepthContextProvider>

View File

@@ -1,6 +1,5 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { useContainerContext, useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
import { useFormElementDnd } from 'features/nodes/components/sidePanel/builder/dnd-hooks';
import { DndListDropIndicator } from 'features/nodes/components/sidePanel/builder/DndListDropIndicator';
@@ -10,9 +9,7 @@ import type { FormElement } from 'features/nodes/types/workflow';
import type { PropsWithChildren } from 'react';
import { memo, useRef } from 'react';
import { useIsRootElement } from './dnd-hooks';
const EDIT_MODE_WRAPPER_CLASS_NAME = getPrefixedId('edit-mode-wrapper', '-');
export const EDIT_MODE_WRAPPER_CLASS_NAME = 'edit-mode-wrapper';
const wrapperSx: SystemStyleObject = {
position: 'relative',
@@ -20,10 +17,6 @@ const wrapperSx: SystemStyleObject = {
'&[data-element-type="divider"]&[data-layout="row"]': {
flex: '0 1 0',
},
'&[data-is-root="true"]': {
w: 'full',
h: 'full',
},
borderRadius: 'base',
};
@@ -71,7 +64,6 @@ export const FormElementEditModeWrapper = memo(({ element, children }: PropsWith
const [activeDropRegion, isDragging] = useFormElementDnd(element.id, draggableRef, dragHandleRef);
const containerCtx = useContainerContext();
const depth = useDepthContext();
const isRootElement = useIsRootElement(element.id);
return (
<Flex
@@ -79,7 +71,6 @@ export const FormElementEditModeWrapper = memo(({ element, children }: PropsWith
ref={draggableRef}
className={EDIT_MODE_WRAPPER_CLASS_NAME}
sx={wrapperSx}
data-is-root={isRootElement}
data-element-type={element.type}
data-layout={containerCtx?.layout}
>
@@ -90,21 +81,10 @@ export const FormElementEditModeWrapper = memo(({ element, children }: PropsWith
data-element-type={element.type}
data-layout={containerCtx?.layout}
>
{!isRootElement && (
// Non-root elements get the header and content wrapper
<>
<FormElementEditModeHeader ref={dragHandleRef} element={element} />
<Flex sx={contentWrapperSx} data-depth={depth}>
{children}
</Flex>
</>
)}
{isRootElement && (
// But the root does not - helps the builder to look less busy
<Flex ref={dragHandleRef} w="full" h="full">
{children}
</Flex>
)}
<FormElementEditModeHeader ref={dragHandleRef} element={element} />
<Flex sx={contentWrapperSx} data-depth={depth}>
{children}
</Flex>
</Flex>
<DndListDropIndicator activeDropRegion={activeDropRegion} gap="var(--invoke-space-4)" />
</Flex>

View File

@@ -23,9 +23,9 @@ import { assert } from 'tsafe';
const sx: SystemStyleObject = {
pt: 3,
w: 'full',
h: 'full',
'&[data-is-empty="true"]': {
w: 'full',
h: 'full',
pt: 0,
},
};

View File

@@ -41,6 +41,7 @@ import type { RefObject } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { flushSync } from 'react-dom';
import type { Param0 } from 'tsafe';
import { assert } from 'tsafe';
const log = logger('dnd');
@@ -329,6 +330,9 @@ export const useFormElementDnd = (
const getAllowedDropRegions = useGetAllowedDropRegions();
useEffect(() => {
if (isRootElement) {
assert(false, 'Root element should not be draggable');
}
const draggableElement = draggableRef.current;
const dragHandleElement = dragHandleRef.current;
@@ -339,8 +343,6 @@ export const useFormElementDnd = (
return combine(
firefoxDndFix(draggableElement),
draggable({
// Don't allow dragging the root element
canDrag: () => !isRootElement,
element: draggableElement,
dragHandle: dragHandleElement,
getInitialData: () => {
@@ -356,7 +358,7 @@ export const useFormElementDnd = (
}),
dropTargetForElements({
element: draggableElement,
getIsSticky: () => !isRootElement,
getIsSticky: () => true,
canDrop: ({ source }) =>
isFormElementDndData(source.data) && source.data.element.id !== getElement(elementId).parentId,
getData: ({ input }) => {
@@ -404,6 +406,52 @@ export const useFormElementDnd = (
return [activeDropRegion, isDragging] as const;
};
export const useRootElementDropTarget = (droppableRef: RefObject<HTMLDivElement>) => {
const [isDraggingOver, setIsDraggingOver] = useState(false);
const getElement = useGetElement();
const getAllowedDropRegions = useGetAllowedDropRegions();
const rootElementId = useAppSelector(selectFormRootElementId);
useEffect(() => {
const droppableElement = droppableRef.current;
if (!droppableElement) {
return;
}
return combine(
dropTargetForElements({
element: droppableElement,
getIsSticky: () => false,
canDrop: ({ source }) =>
getElement(rootElementId, isContainerElement).data.children.length === 0 && isFormElementDndData(source.data),
getData: ({ input }) => {
const element = getElement(rootElementId, isContainerElement);
const targetData = buildFormElementDndData(element);
return attachClosestCenterOrEdge(targetData, {
element: droppableElement,
input,
allowedCenterOrEdge: ['center'],
});
},
onDrag: () => {
setIsDraggingOver(true);
},
onDragLeave: () => {
setIsDraggingOver(false);
},
onDrop: () => {
setIsDraggingOver(false);
},
})
);
}, [droppableRef, getAllowedDropRegions, getElement, rootElementId]);
return isDraggingOver;
};
/**
* Hook that provides dnd functionality for node fields.
*