mirror of
https://github.com/twentyhq/twenty.git
synced 2026-05-19 22:07:21 -04:00
[Website] Make product stepper visuals interactive. (#20602)
We had low-res screenshots for each step in the stepper. Replaced them with interactive components. https://github.com/user-attachments/assets/d03ff924-a1dd-467f-ba19-cece0ecb3486
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -29,6 +29,9 @@ import {
|
||||
ProductStepper,
|
||||
type ProductStepperStepType,
|
||||
} from '@/sections/ProductStepper';
|
||||
import { DataModelVisual } from '@/sections/ProductStepper/visuals/DataModelVisual';
|
||||
import { WorkflowVisual } from '@/sections/ProductStepper/visuals/WorkflowVisual';
|
||||
import { LayoutVisual } from '@/sections/ProductStepper/visuals/LayoutVisual';
|
||||
import { Tabs } from '@/sections/Tabs';
|
||||
import { ThreeCards } from '@/sections/ThreeCards';
|
||||
import { theme } from '@/theme';
|
||||
@@ -58,10 +61,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Data model`)}</HeadingPart>
|
||||
),
|
||||
body: msg`Add objects and fields`,
|
||||
image: {
|
||||
src: '/images/product/stepper/step-one.webp',
|
||||
alt: 'Twenty data model: add objects and fields',
|
||||
},
|
||||
visual: DataModelVisual,
|
||||
},
|
||||
{
|
||||
icon: 'check',
|
||||
@@ -69,10 +69,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Automation`)}</HeadingPart>
|
||||
),
|
||||
body: msg`Create a workflow`,
|
||||
image: {
|
||||
src: '/images/product/stepper/step-two.webp',
|
||||
alt: 'Twenty automation: create a workflow',
|
||||
},
|
||||
visual: WorkflowVisual,
|
||||
},
|
||||
{
|
||||
icon: 'eye',
|
||||
@@ -80,10 +77,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Layout`)}</HeadingPart>
|
||||
),
|
||||
body: msg`Tailor record pages, menus, and views`,
|
||||
image: {
|
||||
src: '/images/product/stepper/step-three.webp',
|
||||
alt: 'Twenty layout: record pages, menus, and views',
|
||||
},
|
||||
visual: LayoutVisual,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export function Flow({ body, children, eyebrow, steps }: FlowProps) {
|
||||
}),
|
||||
);
|
||||
|
||||
const images = steps.map((step) => step.image);
|
||||
const stepVisuals = steps.map(({ visual }) => ({ visual }));
|
||||
|
||||
return (
|
||||
<StyledSection ref={scrollContainerRef}>
|
||||
@@ -86,7 +86,7 @@ export function Flow({ body, children, eyebrow, steps }: FlowProps) {
|
||||
onMobileStepIndexChange={setMobileStepIndex}
|
||||
steps={contentSteps}
|
||||
/>
|
||||
<Visual activeStepIndex={activeStepIndex} images={images} />
|
||||
<Visual activeStepIndex={activeStepIndex} stepVisuals={stepVisuals} />
|
||||
</Grid>
|
||||
</StyledSection>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import type { ImageType } from '@/design-system/components/Image';
|
||||
'use client';
|
||||
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import NextImage from 'next/image';
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
import type { StepperVisualProps } from './types';
|
||||
import { StepperVisualFrame } from './StepperVisualFrame';
|
||||
|
||||
type StepVisual = {
|
||||
visual: ComponentType<StepperVisualProps>;
|
||||
};
|
||||
|
||||
type ProductStepperVisualProps = {
|
||||
activeStepIndex: number;
|
||||
images: ImageType[];
|
||||
stepVisuals: StepVisual[];
|
||||
};
|
||||
|
||||
const PRODUCT_STEPPER_BACKGROUND = '/images/product/stepper/background.webp';
|
||||
@@ -41,19 +48,30 @@ const VisualFrame = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const slideImageClassName = css`
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
const slideClassName = css`
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transition: opacity 0.4s ease;
|
||||
|
||||
&[data-active='true'] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export function Visual({ activeStepIndex, images }: ProductStepperVisualProps) {
|
||||
if (!images || images.length === 0) {
|
||||
const visualWrapperClassName = css`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function Visual({
|
||||
activeStepIndex,
|
||||
stepVisuals,
|
||||
}: ProductStepperVisualProps) {
|
||||
if (!stepVisuals || stepVisuals.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -64,19 +82,20 @@ export function Visual({ activeStepIndex, images }: ProductStepperVisualProps) {
|
||||
backgroundSrc={PRODUCT_STEPPER_BACKGROUND}
|
||||
shapeSrc={PRODUCT_STEPPER_SHAPE}
|
||||
>
|
||||
{images.map((image, index) => {
|
||||
if (!image) return null;
|
||||
{stepVisuals.map((step, index) => {
|
||||
const isActive = index === activeStepIndex;
|
||||
const VisualComponent = step.visual;
|
||||
|
||||
return (
|
||||
<NextImage
|
||||
key={`${image.src}-${index}`}
|
||||
alt={image.alt}
|
||||
className={slideImageClassName}
|
||||
data-active={String(index === activeStepIndex)}
|
||||
fill
|
||||
sizes="(min-width: 921px) 50vw, 100vw"
|
||||
src={image.src}
|
||||
/>
|
||||
<div
|
||||
key={index}
|
||||
className={slideClassName}
|
||||
data-active={String(isActive)}
|
||||
>
|
||||
<div className={visualWrapperClassName}>
|
||||
<VisualComponent active={isActive} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</StepperVisualFrame>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ImageType } from '@/design-system/components/Image';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
|
||||
export type StepperVisualProps = {
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
export type ProductStepperStepType = {
|
||||
body: MessageDescriptor;
|
||||
heading: ReactNode;
|
||||
icon: string;
|
||||
image: ImageType;
|
||||
visual: ComponentType<StepperVisualProps>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
STEPPER_BG,
|
||||
STEPPER_BORDER_MEDIUM,
|
||||
STEPPER_BORDER_SUBTLE,
|
||||
STEPPER_FONT,
|
||||
STEPPER_HEADER_BG,
|
||||
STEPPER_HEADER_BORDER,
|
||||
STEPPER_SHADOW_SM,
|
||||
STEPPER_TEXT,
|
||||
STEPPER_TEXT_MUTED,
|
||||
STEPPER_TEXT_TERTIARY,
|
||||
} from './stepper-visual-tokens';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background: ${STEPPER_BG};
|
||||
border-radius: 2px;
|
||||
box-shadow: ${STEPPER_SHADOW_SM};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: ${STEPPER_FONT};
|
||||
height: 92%;
|
||||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
overflow: hidden;
|
||||
width: 88%;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
`;
|
||||
|
||||
const HeaderLogo = styled.span`
|
||||
align-items: center;
|
||||
background: ${STEPPER_HEADER_BG};
|
||||
border: 1px solid ${STEPPER_HEADER_BORDER};
|
||||
border-radius: 3px;
|
||||
color: ${STEPPER_TEXT};
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
const HeaderTitle = styled.span`
|
||||
color: ${STEPPER_TEXT};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
padding: 0 2px;
|
||||
`;
|
||||
|
||||
const HeaderActions = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const HeaderBtn = styled.span`
|
||||
align-items: center;
|
||||
border: 1px solid ${STEPPER_BORDER_MEDIUM};
|
||||
border-radius: 4px;
|
||||
color: ${STEPPER_TEXT_MUTED};
|
||||
display: flex;
|
||||
height: 22px;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
`;
|
||||
|
||||
const HeaderCmdBtn = styled.span`
|
||||
align-items: center;
|
||||
border: 1px solid ${STEPPER_BORDER_SUBTLE};
|
||||
border-radius: 4px;
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
gap: 4px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
`;
|
||||
|
||||
export const ShellCanvas = styled.div`
|
||||
background: white;
|
||||
border: 1px solid ${STEPPER_BORDER_MEDIUM};
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
margin: 0 10px 10px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const ShellSvgLayer = styled.svg`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type AppPreviewShellProps = {
|
||||
active: boolean;
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function AppPreviewShell({
|
||||
active,
|
||||
children,
|
||||
title,
|
||||
}: AppPreviewShellProps) {
|
||||
return (
|
||||
<Wrapper style={{ opacity: active ? 1 : 0.7, transition: 'opacity 0.3s' }}>
|
||||
<Header>
|
||||
<HeaderLogo>
|
||||
<svg
|
||||
fill="none"
|
||||
height="9"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="9"
|
||||
>
|
||||
<path d="M10 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M19.5 13a2 2 0 1 0 0 4a2 2 0 0 0 0 -4" />
|
||||
<path d="M4.5 13a2 2 0 1 0 0 4a2 2 0 0 0 0 -4" />
|
||||
<path d="M12 7v4" />
|
||||
<path d="M6.5 13l5.5 -2l5.5 2" />
|
||||
</svg>
|
||||
</HeaderLogo>
|
||||
<HeaderTitle>{title}</HeaderTitle>
|
||||
<HeaderActions>
|
||||
<HeaderBtn>
|
||||
<svg
|
||||
fill="none"
|
||||
height="14"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
>
|
||||
<path d="M6 15l6 -6l6 6" />
|
||||
</svg>
|
||||
</HeaderBtn>
|
||||
<HeaderBtn>
|
||||
<svg
|
||||
fill="none"
|
||||
height="14"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
>
|
||||
<path d="M6 9l6 6l6 -6" />
|
||||
</svg>
|
||||
</HeaderBtn>
|
||||
<HeaderCmdBtn>
|
||||
<svg
|
||||
fill="none"
|
||||
height="12"
|
||||
stroke="#666"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
viewBox="0 0 24 24"
|
||||
width="12"
|
||||
>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
⌘K
|
||||
</HeaderCmdBtn>
|
||||
</HeaderActions>
|
||||
</Header>
|
||||
{children}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import type { StepperVisualProps } from '../types';
|
||||
|
||||
import { AppPreviewShell, ShellCanvas, ShellSvgLayer } from './AppPreviewShell';
|
||||
import {
|
||||
BADGE_CUSTOM_BG,
|
||||
BADGE_CUSTOM_BORDER,
|
||||
BADGE_CUSTOM_TEXT,
|
||||
BADGE_STANDARD_BG,
|
||||
BADGE_STANDARD_BORDER,
|
||||
BADGE_STANDARD_TEXT,
|
||||
CONNECTIONS,
|
||||
type ConnectionDef,
|
||||
ENTITIES,
|
||||
} from './data/DataModel.data';
|
||||
import { DrawEdge } from './DrawEdge';
|
||||
import { IconChevronDown } from './icons/DataModelIcons';
|
||||
import {
|
||||
STEPPER_BORDER_LIGHT,
|
||||
STEPPER_BORDER_MEDIUM,
|
||||
STEPPER_BORDER_STRONG,
|
||||
STEPPER_CARD_BG,
|
||||
STEPPER_TEXT,
|
||||
STEPPER_TEXT_MUTED,
|
||||
STEPPER_TEXT_TERTIARY,
|
||||
} from './stepper-visual-tokens';
|
||||
|
||||
const EntityCard = styled.div<{ $hovered: boolean }>`
|
||||
backdrop-filter: blur(14px);
|
||||
background: ${STEPPER_CARD_BG};
|
||||
border: 1px solid
|
||||
${({ $hovered }) =>
|
||||
$hovered ? STEPPER_BORDER_STRONG : STEPPER_BORDER_MEDIUM};
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 0 2px rgba(0, 0, 0, 0.08),
|
||||
0 2px 2px rgba(0, 0, 0, 0.04);
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 130px;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
touch-action: none;
|
||||
transition: border-color 0.15s ease;
|
||||
z-index: 2;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
z-index: 10;
|
||||
}
|
||||
`;
|
||||
|
||||
const EntityHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const InnerCard = styled.div`
|
||||
background: white;
|
||||
border: 1px solid ${STEPPER_BORDER_LIGHT};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
padding: 4px 0;
|
||||
`;
|
||||
|
||||
const EntityIcon = styled.span<{ $bg: string; $border: string }>`
|
||||
align-items: center;
|
||||
background: ${({ $bg }) => $bg};
|
||||
border: 1px solid ${({ $border }) => $border};
|
||||
border-radius: 3px;
|
||||
color: ${STEPPER_TEXT};
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
const EntityLabel = styled.span`
|
||||
color: ${STEPPER_TEXT};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const EntityMeta = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
`;
|
||||
|
||||
const MetaBadge = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const MetaBadgeIcon = styled.span<{
|
||||
$bg: string;
|
||||
$border: string;
|
||||
$color: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ $bg }) => $bg};
|
||||
border: 1px solid ${({ $border }) => $border};
|
||||
border-radius: 2px;
|
||||
color: ${({ $color }) => $color};
|
||||
display: flex;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
const MetaBadgeText = styled.span`
|
||||
color: ${STEPPER_TEXT_MUTED};
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
const FieldRow = styled.div`
|
||||
align-items: center;
|
||||
color: ${STEPPER_TEXT};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
gap: 4px;
|
||||
height: 22px;
|
||||
line-height: 1.4;
|
||||
padding: 0 6px;
|
||||
`;
|
||||
|
||||
const FieldIcon = styled.span`
|
||||
align-items: center;
|
||||
color: ${STEPPER_TEXT_MUTED};
|
||||
display: flex;
|
||||
height: 14px;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
`;
|
||||
|
||||
const ExpandHint = styled.div`
|
||||
align-items: center;
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
gap: 4px;
|
||||
height: 22px;
|
||||
line-height: 1.4;
|
||||
padding: 0 6px;
|
||||
`;
|
||||
|
||||
const CARD_WIDTH = 140;
|
||||
const CARD_HEIGHT_ESTIMATE = 110;
|
||||
|
||||
function getCardCenter(
|
||||
positions: Record<string, { x: number; y: number }>,
|
||||
entityId: string,
|
||||
): { x: number; y: number } {
|
||||
const pos = positions[entityId];
|
||||
if (!pos) return { x: 0, y: 0 };
|
||||
return { x: pos.x + CARD_WIDTH / 2, y: pos.y + CARD_HEIGHT_ESTIMATE / 2 };
|
||||
}
|
||||
|
||||
export function DataModelVisual({ active }: StepperVisualProps) {
|
||||
const [positions, setPositions] = useState<
|
||||
Record<string, { x: number; y: number }>
|
||||
>(() =>
|
||||
Object.fromEntries(
|
||||
ENTITIES.map((entity) => [entity.id, { x: entity.x, y: entity.y }]),
|
||||
),
|
||||
);
|
||||
const [hoveredEntity, setHoveredEntity] = useState<string | null>(null);
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
const dragStartRef = useRef<{
|
||||
entityId: string;
|
||||
posX: number;
|
||||
posY: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
} | null>(null);
|
||||
|
||||
const handlePointerDown = (entityId: string, event: React.PointerEvent) => {
|
||||
event.preventDefault();
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
const pos = positions[entityId];
|
||||
dragStartRef.current = {
|
||||
entityId,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
posX: pos.x,
|
||||
posY: pos.y,
|
||||
};
|
||||
setDragging(entityId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: React.PointerEvent) => {
|
||||
if (!dragStartRef.current) return;
|
||||
const { entityId, startX, startY, posX, posY } = dragStartRef.current;
|
||||
const dx = event.clientX - startX;
|
||||
const dy = event.clientY - startY;
|
||||
setPositions((prev) => ({
|
||||
...prev,
|
||||
[entityId]: { x: posX + dx, y: posY + dy },
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
dragStartRef.current = null;
|
||||
setDragging(null);
|
||||
};
|
||||
|
||||
const isConnectionHighlighted = (connection: ConnectionDef) =>
|
||||
hoveredEntity === connection.from || hoveredEntity === connection.to;
|
||||
|
||||
return (
|
||||
<AppPreviewShell active={active} title="Data model">
|
||||
<ShellCanvas
|
||||
onPointerCancel={handlePointerUp}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<ShellSvgLayer>
|
||||
{CONNECTIONS.map((conn) => (
|
||||
<DrawEdge
|
||||
key={`${conn.from}-${conn.to}`}
|
||||
circleR={1.5}
|
||||
elbow="horizontal-first"
|
||||
from={getCardCenter(positions, conn.from)}
|
||||
highlighted={isConnectionHighlighted(conn)}
|
||||
to={getCardCenter(positions, conn.to)}
|
||||
/>
|
||||
))}
|
||||
</ShellSvgLayer>
|
||||
|
||||
{ENTITIES.map((entity) => {
|
||||
const pos = positions[entity.id];
|
||||
const isHovered = hoveredEntity === entity.id;
|
||||
return (
|
||||
<EntityCard
|
||||
key={entity.id}
|
||||
$hovered={isHovered || dragging === entity.id}
|
||||
onPointerDown={(event) => handlePointerDown(entity.id, event)}
|
||||
onPointerEnter={() => setHoveredEntity(entity.id)}
|
||||
onPointerLeave={() => setHoveredEntity(null)}
|
||||
style={{
|
||||
left: pos.x,
|
||||
top: pos.y,
|
||||
transition:
|
||||
dragging === entity.id ? 'none' : 'border-color 0.15s',
|
||||
}}
|
||||
>
|
||||
<EntityHeader>
|
||||
<EntityIcon $bg={entity.iconBg} $border={entity.iconBorder}>
|
||||
{entity.headerIcon}
|
||||
</EntityIcon>
|
||||
<EntityLabel>{entity.label}</EntityLabel>
|
||||
<EntityMeta>· {entity.meta}</EntityMeta>
|
||||
<MetaBadge>
|
||||
<MetaBadgeIcon
|
||||
$bg={entity.isCustom ? BADGE_CUSTOM_BG : BADGE_STANDARD_BG}
|
||||
$border={
|
||||
entity.isCustom
|
||||
? BADGE_CUSTOM_BORDER
|
||||
: BADGE_STANDARD_BORDER
|
||||
}
|
||||
$color={
|
||||
entity.isCustom ? BADGE_CUSTOM_TEXT : BADGE_STANDARD_TEXT
|
||||
}
|
||||
>
|
||||
L
|
||||
</MetaBadgeIcon>
|
||||
<MetaBadgeText>
|
||||
{entity.isCustom ? 'Custom' : 'Standard'}
|
||||
</MetaBadgeText>
|
||||
</MetaBadge>
|
||||
</EntityHeader>
|
||||
<InnerCard>
|
||||
{entity.fields.map((field) => (
|
||||
<FieldRow key={field.label}>
|
||||
<FieldIcon>{field.icon}</FieldIcon>
|
||||
{field.label}
|
||||
</FieldRow>
|
||||
))}
|
||||
<ExpandHint>
|
||||
<IconChevronDown /> {entity.expandCount} fields
|
||||
</ExpandHint>
|
||||
</InnerCard>
|
||||
</EntityCard>
|
||||
);
|
||||
})}
|
||||
</ShellCanvas>
|
||||
</AppPreviewShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
STEPPER_BORDER_MEDIUM,
|
||||
STEPPER_BORDER_STRONG,
|
||||
} from './stepper-visual-tokens';
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
|
||||
type DrawEdgeProps = {
|
||||
circleR?: number;
|
||||
elbow?: 'horizontal-first' | 'vertical-first';
|
||||
from: Point;
|
||||
highlighted: boolean;
|
||||
to: Point;
|
||||
};
|
||||
|
||||
export function DrawEdge({
|
||||
circleR = 2,
|
||||
elbow = 'vertical-first',
|
||||
from,
|
||||
highlighted,
|
||||
to,
|
||||
}: DrawEdgeProps) {
|
||||
const color = highlighted ? STEPPER_BORDER_STRONG : STEPPER_BORDER_MEDIUM;
|
||||
const dx = Math.abs(to.x - from.x);
|
||||
const dy = Math.abs(to.y - from.y);
|
||||
|
||||
let pathD: string;
|
||||
let startX = from.x;
|
||||
let startY = from.y;
|
||||
let endX = to.x;
|
||||
let endY = to.y;
|
||||
|
||||
if (dx < 30) {
|
||||
const avgX = (from.x + to.x) / 2;
|
||||
startX = avgX;
|
||||
endX = avgX;
|
||||
pathD = `M${avgX},${from.y} L${avgX},${to.y}`;
|
||||
} else if (dy < 30) {
|
||||
const avgY = (from.y + to.y) / 2;
|
||||
startY = avgY;
|
||||
endY = avgY;
|
||||
pathD = `M${from.x},${avgY} L${to.x},${avgY}`;
|
||||
} else if (elbow === 'horizontal-first') {
|
||||
pathD = `M${from.x},${from.y} L${to.x},${from.y} L${to.x},${to.y}`;
|
||||
} else {
|
||||
pathD = `M${from.x},${from.y} L${from.x},${to.y} L${to.x},${to.y}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
d={pathD}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={0.75}
|
||||
style={{ transition: 'stroke 0.15s' }}
|
||||
/>
|
||||
<circle cx={startX} cy={startY} fill={color} r={circleR} />
|
||||
<circle cx={endX} cy={endY} fill={color} r={circleR} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import type { StepperVisualProps } from '../types';
|
||||
|
||||
import { FIELDS, NAV } from './data/layout.data';
|
||||
import {
|
||||
CheckSvg,
|
||||
ChevLeft,
|
||||
DotsV,
|
||||
DotsVW,
|
||||
EyeIcon,
|
||||
FieldIcon,
|
||||
GripV,
|
||||
ListSvg,
|
||||
NavSvgIcon,
|
||||
NewSecSvg,
|
||||
PaintSvg,
|
||||
PlusSvg,
|
||||
SparkSvg,
|
||||
} from './icons/LayoutIcons';
|
||||
import {
|
||||
ActionBtn,
|
||||
ActionsBar,
|
||||
BlueHeader,
|
||||
Canvas,
|
||||
HeaderCenter,
|
||||
HeaderLeft,
|
||||
HeaderSave,
|
||||
HeaderTitle,
|
||||
MainCard,
|
||||
NavBreadcrumb,
|
||||
NavChevron,
|
||||
NavIconBox,
|
||||
NavItem,
|
||||
NavPanel,
|
||||
NavSectionLabel,
|
||||
NavSubItem,
|
||||
NavSuffix,
|
||||
RightPanel,
|
||||
RPActionBtn,
|
||||
RPAddIconBox,
|
||||
RPAddSection,
|
||||
RPAddText,
|
||||
RPBackBtn,
|
||||
RPDoneBtn,
|
||||
RPEditable,
|
||||
RPEditText,
|
||||
RPFieldDot,
|
||||
RPFieldIconBox,
|
||||
RPFieldLabels,
|
||||
RPFieldName,
|
||||
RPFieldRow,
|
||||
RPFieldType,
|
||||
RPFields,
|
||||
RPHeader,
|
||||
RPIconBox,
|
||||
RPNewDesc,
|
||||
RPNewFields,
|
||||
RPNewTitle,
|
||||
RPSectionName,
|
||||
RPSectionRow,
|
||||
RPSubBar,
|
||||
RPSubLabel,
|
||||
RPTitleBold,
|
||||
RPTitleGroup,
|
||||
RPTitleSub,
|
||||
WChip,
|
||||
WIcon,
|
||||
WLabel,
|
||||
WRow,
|
||||
WSectionLabel,
|
||||
WValue,
|
||||
WidgetInner,
|
||||
WidgetPanel,
|
||||
WidgetTitle,
|
||||
} from './layout-styles';
|
||||
|
||||
export function LayoutVisual({ active }: StepperVisualProps) {
|
||||
const [fields, setFields] = useState(FIELDS);
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
const dragStartY = useRef(0);
|
||||
|
||||
const toggleVisibility = (fieldId: string) => {
|
||||
setFields((prev) =>
|
||||
prev.map((f) => (f.id === fieldId ? { ...f, visible: !f.visible } : f)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleDragStart = (fieldId: string, event: React.PointerEvent) => {
|
||||
event.preventDefault();
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
setDraggingId(fieldId);
|
||||
dragStartY.current = event.clientY;
|
||||
};
|
||||
|
||||
const handleDragMove = (event: React.PointerEvent) => {
|
||||
if (!draggingId) return;
|
||||
const dy = event.clientY - dragStartY.current;
|
||||
const steps = Math.round(dy / 22);
|
||||
if (steps === 0) return;
|
||||
setFields((prev) => {
|
||||
const idx = prev.findIndex((f) => f.id === draggingId);
|
||||
if (idx === -1) return prev;
|
||||
const to = Math.max(0, Math.min(prev.length - 1, idx + steps));
|
||||
if (to === idx) return prev;
|
||||
const next = [...prev];
|
||||
const [moved] = next.splice(idx, 1);
|
||||
next.splice(to, 0, moved);
|
||||
return next;
|
||||
});
|
||||
dragStartY.current = event.clientY;
|
||||
};
|
||||
|
||||
const handleDragEnd = () => setDraggingId(null);
|
||||
const sections = [...new Set(fields.map((f) => f.section))];
|
||||
|
||||
return (
|
||||
<Canvas style={{ opacity: active ? 1 : 0.6, transition: 'opacity 0.3s' }}>
|
||||
<MainCard>
|
||||
<BlueHeader>
|
||||
<HeaderLeft>
|
||||
<DotsVW />
|
||||
</HeaderLeft>
|
||||
<HeaderCenter>
|
||||
<PaintSvg />
|
||||
<HeaderTitle>Layout edition</HeaderTitle>
|
||||
</HeaderCenter>
|
||||
<HeaderSave>
|
||||
<CheckSvg /> Save
|
||||
</HeaderSave>
|
||||
</BlueHeader>
|
||||
</MainCard>
|
||||
|
||||
<WidgetPanel>
|
||||
<WidgetInner>
|
||||
<WidgetTitle>Widget name</WidgetTitle>
|
||||
<WSectionLabel>General</WSectionLabel>
|
||||
<WRow>
|
||||
<WIcon>
|
||||
<FieldIcon type="link" />
|
||||
</WIcon>
|
||||
<WLabel>URL</WLabel>
|
||||
<WChip>qonto.com</WChip>
|
||||
</WRow>
|
||||
<WRow>
|
||||
<WIcon>
|
||||
<FieldIcon type="user" />
|
||||
</WIcon>
|
||||
<WLabel>Account O...</WLabel>
|
||||
<WValue>Phil Schiller</WValue>
|
||||
</WRow>
|
||||
<WRow>
|
||||
<WIcon>
|
||||
<FieldIcon type="map" />
|
||||
</WIcon>
|
||||
<WLabel>Address</WLabel>
|
||||
<WValue>18 Rue De Navarin, 750...</WValue>
|
||||
</WRow>
|
||||
<WRow>
|
||||
<WIcon>
|
||||
<FieldIcon type="target" />
|
||||
</WIcon>
|
||||
<WLabel>ICP</WLabel>
|
||||
<WValue>✓ True</WValue>
|
||||
</WRow>
|
||||
</WidgetInner>
|
||||
</WidgetPanel>
|
||||
|
||||
<NavPanel>
|
||||
<NavSectionLabel>Workspace</NavSectionLabel>
|
||||
{NAV.map((item) => (
|
||||
<div key={item.label}>
|
||||
<NavItem $active={item.active}>
|
||||
<NavIconBox $bg={item.bg}>
|
||||
<NavSvgIcon type={item.icon} />
|
||||
</NavIconBox>
|
||||
{item.label}
|
||||
{'suffix' in item && item.suffix && (
|
||||
<NavSuffix>· {item.suffix}</NavSuffix>
|
||||
)}
|
||||
{'folder' in item && item.folder && <NavChevron>▾</NavChevron>}
|
||||
</NavItem>
|
||||
{'children' in item &&
|
||||
item.children?.map((child) => (
|
||||
<NavSubItem key={child.label}>
|
||||
<NavBreadcrumb />
|
||||
<NavIconBox $bg={child.bg}>
|
||||
<NavSvgIcon type={child.icon} />
|
||||
</NavIconBox>
|
||||
{child.label}
|
||||
</NavSubItem>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</NavPanel>
|
||||
|
||||
<ActionsBar>
|
||||
<ActionBtn>+ New record</ActionBtn>
|
||||
<ActionBtn>✧ Enrich</ActionBtn>
|
||||
<ActionBtn>✎ Edit actions</ActionBtn>
|
||||
</ActionsBar>
|
||||
|
||||
<RightPanel
|
||||
onPointerCancel={handleDragEnd}
|
||||
onPointerMove={handleDragMove}
|
||||
onPointerUp={handleDragEnd}
|
||||
>
|
||||
<RPHeader>
|
||||
<RPBackBtn>
|
||||
<ChevLeft />
|
||||
</RPBackBtn>
|
||||
<RPIconBox>
|
||||
<ListSvg />
|
||||
</RPIconBox>
|
||||
<RPTitleGroup>
|
||||
<RPTitleBold>Fields</RPTitleBold>
|
||||
<RPTitleSub>Fields widget</RPTitleSub>
|
||||
</RPTitleGroup>
|
||||
<RPActionBtn>
|
||||
<SparkSvg />
|
||||
</RPActionBtn>
|
||||
</RPHeader>
|
||||
|
||||
<RPSubBar>
|
||||
<RPBackBtn>
|
||||
<ChevLeft />
|
||||
</RPBackBtn>
|
||||
<RPSubLabel>Layout</RPSubLabel>
|
||||
</RPSubBar>
|
||||
|
||||
<RPFields>
|
||||
{sections.map((section, si) => (
|
||||
<div key={section}>
|
||||
<RPSectionRow>
|
||||
<GripV />
|
||||
<RPSectionName>{section}</RPSectionName>
|
||||
<RPActionBtn>
|
||||
<DotsV />
|
||||
</RPActionBtn>
|
||||
</RPSectionRow>
|
||||
|
||||
{si === 0 && (
|
||||
<RPEditable>
|
||||
<RPEditText>Industry</RPEditText>
|
||||
<RPDoneBtn>Done</RPDoneBtn>
|
||||
</RPEditable>
|
||||
)}
|
||||
|
||||
{fields
|
||||
.filter((f) => f.section === section)
|
||||
.map((field) => (
|
||||
<RPFieldRow
|
||||
key={field.id}
|
||||
$dragging={draggingId === field.id}
|
||||
onPointerDown={(e) => handleDragStart(field.id, e)}
|
||||
>
|
||||
<RPFieldIconBox>
|
||||
<FieldIcon type={field.icon} />
|
||||
</RPFieldIconBox>
|
||||
<RPFieldLabels>
|
||||
<RPFieldName>{field.label}</RPFieldName>
|
||||
<RPFieldDot>·</RPFieldDot>
|
||||
<RPFieldType>{field.type}</RPFieldType>
|
||||
</RPFieldLabels>
|
||||
<RPActionBtn
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleVisibility(field.id);
|
||||
}}
|
||||
>
|
||||
<EyeIcon visible={field.visible} />
|
||||
</RPActionBtn>
|
||||
<RPActionBtn>
|
||||
<DotsV />
|
||||
</RPActionBtn>
|
||||
</RPFieldRow>
|
||||
))}
|
||||
|
||||
{si > 0 && si < sections.length - 1 && (
|
||||
<RPAddSection>
|
||||
<RPAddIconBox>
|
||||
<NewSecSvg />
|
||||
</RPAddIconBox>
|
||||
<RPAddText>Add a Section</RPAddText>
|
||||
</RPAddSection>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<RPNewFields>
|
||||
<RPAddIconBox>
|
||||
<PlusSvg />
|
||||
</RPAddIconBox>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<RPNewTitle>New fields</RPNewTitle>
|
||||
<RPNewDesc>Default position/visibility for field…</RPNewDesc>
|
||||
</div>
|
||||
</RPNewFields>
|
||||
|
||||
<RPAddSection>
|
||||
<RPAddIconBox>
|
||||
<NewSecSvg />
|
||||
</RPAddIconBox>
|
||||
<RPAddText>Add a Section</RPAddText>
|
||||
</RPAddSection>
|
||||
</RPFields>
|
||||
</RightPanel>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
'use client';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import type { StepperVisualProps } from '../types';
|
||||
|
||||
import { AppPreviewShell, ShellCanvas, ShellSvgLayer } from './AppPreviewShell';
|
||||
import {
|
||||
COLOR_GRAY,
|
||||
COLOR_GRAY_BG,
|
||||
COLOR_GREEN,
|
||||
COLOR_TEAL_BG,
|
||||
EDGES,
|
||||
NODE_HEIGHT,
|
||||
NODE_WIDTH,
|
||||
NODES,
|
||||
} from './data/workflow.data';
|
||||
import { DrawEdge } from './DrawEdge';
|
||||
import { CheckIcon, NodeIcon } from './icons/WorkflowIcons';
|
||||
import {
|
||||
STEPPER_BORDER_STRONG,
|
||||
STEPPER_CARD_BG,
|
||||
STEPPER_TEXT,
|
||||
STEPPER_TEXT_MUTED,
|
||||
STEPPER_TEXT_TERTIARY,
|
||||
STEPPER_TINT,
|
||||
} from './stepper-visual-tokens';
|
||||
import { useWorkflowAnimation } from './use-workflow-animation';
|
||||
|
||||
const NodeCard = styled.div`
|
||||
align-items: center;
|
||||
background: ${STEPPER_CARD_BG};
|
||||
border: 1px solid ${STEPPER_BORDER_STRONG};
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 0 2px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
min-width: ${NODE_WIDTH}px;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const NodeIconBox = styled.div`
|
||||
align-items: center;
|
||||
background: ${STEPPER_TINT};
|
||||
border-radius: 4px;
|
||||
color: ${STEPPER_TEXT_MUTED};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
`;
|
||||
|
||||
const NodeRight = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const NodeLabelRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
height: 13px;
|
||||
`;
|
||||
|
||||
const NodeLabel = styled.span<{ $color: string }>`
|
||||
color: ${({ $color }) => $color};
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const NodeCheck = styled.span<{ $bg: string; $visible: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ $bg }) => $bg};
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
height: 12px;
|
||||
justify-content: center;
|
||||
opacity: ${({ $visible }) => ($visible ? 1 : 0)};
|
||||
transition: opacity 0.3s;
|
||||
width: 12px;
|
||||
`;
|
||||
|
||||
const NodeName = styled.div<{ $dimmed?: boolean }>`
|
||||
color: ${({ $dimmed }) => ($dimmed ? STEPPER_TEXT_TERTIARY : STEPPER_TEXT)};
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const IterationLabel = styled.span`
|
||||
background: ${STEPPER_CARD_BG};
|
||||
border: 1px solid ${STEPPER_BORDER_STRONG};
|
||||
border-radius: 4px;
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
padding: 2px 4px;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 3;
|
||||
`;
|
||||
|
||||
function getNodeCenter(pos: { x: number; y: number }): {
|
||||
x: number;
|
||||
y: number;
|
||||
} {
|
||||
return { x: pos.x + NODE_WIDTH / 2, y: pos.y + NODE_HEIGHT / 2 };
|
||||
}
|
||||
|
||||
export function WorkflowVisual({ active }: StepperVisualProps) {
|
||||
const [positions, setPositions] = useState<
|
||||
Record<string, { x: number; y: number }>
|
||||
>(() =>
|
||||
Object.fromEntries(
|
||||
NODES.map((node) => [node.id, { x: node.x, y: node.y }]),
|
||||
),
|
||||
);
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
const dragStartRef = useRef<{
|
||||
nodeId: string;
|
||||
posX: number;
|
||||
posY: number;
|
||||
startX: number;
|
||||
startY: number;
|
||||
} | null>(null);
|
||||
|
||||
const activeNodes = useWorkflowAnimation(active);
|
||||
|
||||
const handlePointerDown = (nodeId: string, event: React.PointerEvent) => {
|
||||
event.preventDefault();
|
||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||
const pos = positions[nodeId];
|
||||
dragStartRef.current = {
|
||||
nodeId,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
posX: pos.x,
|
||||
posY: pos.y,
|
||||
};
|
||||
setDragging(nodeId);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: React.PointerEvent) => {
|
||||
if (!dragStartRef.current) return;
|
||||
const { nodeId, startX, startY, posX, posY } = dragStartRef.current;
|
||||
const dx = event.clientX - startX;
|
||||
const dy = event.clientY - startY;
|
||||
setPositions((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: { x: posX + dx, y: posY + dy },
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
dragStartRef.current = null;
|
||||
setDragging(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppPreviewShell active={active} title="Workflow Runs">
|
||||
<ShellCanvas
|
||||
onPointerCancel={handlePointerUp}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<ShellSvgLayer>
|
||||
{EDGES.map((edge) => {
|
||||
const fromPos = positions[edge.from];
|
||||
const toPos = positions[edge.to];
|
||||
if (!fromPos || !toPos) return null;
|
||||
return (
|
||||
<DrawEdge
|
||||
key={`${edge.from}-${edge.to}`}
|
||||
circleR={2.5}
|
||||
from={getNodeCenter(fromPos)}
|
||||
highlighted={
|
||||
activeNodes.has(edge.from) && activeNodes.has(edge.to)
|
||||
}
|
||||
to={getNodeCenter(toPos)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{(() => {
|
||||
const iterPos = positions['iterator'];
|
||||
const emailPos = positions['email'];
|
||||
if (!iterPos || !emailPos) return null;
|
||||
|
||||
const startX = iterPos.x + NODE_WIDTH;
|
||||
const startY = iterPos.y + NODE_HEIGHT / 2;
|
||||
const horizEnd = startX + 117;
|
||||
const r = 8;
|
||||
const vertEnd = emailPos.y;
|
||||
const emailCenterX = emailPos.x + NODE_WIDTH / 2;
|
||||
const midY = vertEnd - 28;
|
||||
const loopColor = STEPPER_BORDER_STRONG;
|
||||
const ah = 3;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<circle cx={startX} cy={startY} fill={loopColor} r={2.5} />
|
||||
<path
|
||||
d={[
|
||||
`M${startX},${startY}`,
|
||||
`H${horizEnd - r}`,
|
||||
`Q${horizEnd},${startY} ${horizEnd},${startY + r}`,
|
||||
`V${midY - r}`,
|
||||
`Q${horizEnd},${midY} ${horizEnd - r},${midY}`,
|
||||
`H${emailCenterX + r}`,
|
||||
`Q${emailCenterX},${midY} ${emailCenterX},${midY + r}`,
|
||||
`V${vertEnd}`,
|
||||
].join(' ')}
|
||||
fill="none"
|
||||
stroke={loopColor}
|
||||
strokeWidth={0.75}
|
||||
/>
|
||||
<path
|
||||
d={`M${emailCenterX - ah},${vertEnd - ah} L${emailCenterX},${vertEnd} L${emailCenterX + ah},${vertEnd - ah}`}
|
||||
fill="none"
|
||||
stroke={loopColor}
|
||||
strokeWidth={0.75}
|
||||
/>
|
||||
<circle
|
||||
cx={emailCenterX}
|
||||
cy={vertEnd}
|
||||
fill={loopColor}
|
||||
r={2.5}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
</ShellSvgLayer>
|
||||
|
||||
{NODES.map((node) => {
|
||||
const pos = positions[node.id];
|
||||
const isActive = activeNodes.has(node.id);
|
||||
const checkBg = node.dimmed ? COLOR_GRAY_BG : COLOR_TEAL_BG;
|
||||
const checkColor = node.dimmed ? COLOR_GRAY : COLOR_GREEN;
|
||||
return (
|
||||
<NodeCard
|
||||
key={node.id}
|
||||
onPointerDown={(event) => handlePointerDown(node.id, event)}
|
||||
style={{
|
||||
cursor: dragging === node.id ? 'grabbing' : 'grab',
|
||||
left: pos.x,
|
||||
top: pos.y,
|
||||
transition: dragging === node.id ? 'none' : 'box-shadow 0.15s',
|
||||
}}
|
||||
>
|
||||
<NodeIconBox>
|
||||
<NodeIcon name={node.icon} />
|
||||
</NodeIconBox>
|
||||
<NodeRight>
|
||||
<NodeLabelRow>
|
||||
<NodeLabel $color={node.labelColor}>{node.type}</NodeLabel>
|
||||
{node.badge && (
|
||||
<NodeLabel $color={node.labelColor}>{node.badge}</NodeLabel>
|
||||
)}
|
||||
{node.badge && (
|
||||
<NodeCheck $bg={checkBg} $visible={isActive}>
|
||||
<CheckIcon color={checkColor} />
|
||||
</NodeCheck>
|
||||
)}
|
||||
</NodeLabelRow>
|
||||
<NodeName $dimmed={node.dimmed}>{node.label}</NodeName>
|
||||
</NodeRight>
|
||||
</NodeCard>
|
||||
);
|
||||
})}
|
||||
|
||||
<IterationLabel
|
||||
style={{
|
||||
left: positions['iterator'].x + NODE_WIDTH + 10,
|
||||
top: positions['iterator'].y + NODE_HEIGHT / 2 - 9,
|
||||
}}
|
||||
>
|
||||
iteration 2/3
|
||||
</IterationLabel>
|
||||
</ShellCanvas>
|
||||
</AppPreviewShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
IconApps,
|
||||
IconBuilding,
|
||||
IconBuildingSm,
|
||||
IconTag,
|
||||
IconTarget,
|
||||
IconUser,
|
||||
IconUserScreenSm,
|
||||
IconUserSm,
|
||||
IconUsersSm,
|
||||
IconTargetSm,
|
||||
} from '../icons/DataModelIcons';
|
||||
|
||||
const COLOR_INDIGO_BG = '#d9e2fc';
|
||||
const COLOR_INDIGO_BORDER = '#c6d4f9';
|
||||
const COLOR_PURPLE_BG = '#eddbf9';
|
||||
const COLOR_PURPLE_BORDER = '#e3ccf4';
|
||||
const COLOR_RED_BG = '#fdd8d8';
|
||||
const COLOR_RED_BORDER = '#f9c6c6';
|
||||
const COLOR_GREEN_BG = '#d4f4e2';
|
||||
const COLOR_GREEN_BORDER = '#b4e7cf';
|
||||
|
||||
export const BADGE_STANDARD_BG = '#f0f4ff';
|
||||
export const BADGE_STANDARD_BORDER = '#e6edfe';
|
||||
export const BADGE_STANDARD_TEXT = '#3e63dd';
|
||||
export const BADGE_CUSTOM_BG = '#fff1e7';
|
||||
export const BADGE_CUSTOM_BORDER = '#ffe8d7';
|
||||
export const BADGE_CUSTOM_TEXT = '#f76808';
|
||||
|
||||
export type EntityDef = {
|
||||
expandCount: number;
|
||||
fields: { icon: ReactNode; label: string }[];
|
||||
headerIcon: ReactNode;
|
||||
iconBg: string;
|
||||
iconBorder: string;
|
||||
id: string;
|
||||
isCustom: boolean;
|
||||
label: string;
|
||||
meta: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type ConnectionDef = {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
export const ENTITIES: EntityDef[] = [
|
||||
{
|
||||
id: 'workspaces',
|
||||
label: 'Workspaces',
|
||||
meta: '22',
|
||||
isCustom: true,
|
||||
headerIcon: <IconUserScreenSm />,
|
||||
iconBg: COLOR_GREEN_BG,
|
||||
iconBorder: COLOR_GREEN_BORDER,
|
||||
fields: [
|
||||
{ icon: <IconBuilding />, label: 'Company' },
|
||||
{ icon: <IconUser />, label: 'Users' },
|
||||
],
|
||||
expandCount: 21,
|
||||
x: 40,
|
||||
y: 40,
|
||||
},
|
||||
{
|
||||
id: 'companies',
|
||||
label: 'Companies',
|
||||
meta: '39',
|
||||
isCustom: false,
|
||||
headerIcon: <IconBuildingSm />,
|
||||
iconBg: COLOR_INDIGO_BG,
|
||||
iconBorder: COLOR_INDIGO_BORDER,
|
||||
fields: [
|
||||
{ icon: <IconApps />, label: 'Workspace' },
|
||||
{ icon: <IconTag />, label: '31 fields' },
|
||||
],
|
||||
expandCount: 8,
|
||||
x: 290,
|
||||
y: 20,
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
label: 'Users',
|
||||
meta: '497',
|
||||
isCustom: true,
|
||||
headerIcon: <IconUsersSm />,
|
||||
iconBg: COLOR_PURPLE_BG,
|
||||
iconBorder: COLOR_PURPLE_BORDER,
|
||||
fields: [
|
||||
{ icon: <IconUser />, label: 'People' },
|
||||
{ icon: <IconApps />, label: 'Workspace' },
|
||||
],
|
||||
expandCount: 32,
|
||||
x: 40,
|
||||
y: 310,
|
||||
},
|
||||
{
|
||||
id: 'people',
|
||||
label: 'People',
|
||||
meta: '648',
|
||||
isCustom: false,
|
||||
headerIcon: <IconUserSm />,
|
||||
iconBg: COLOR_INDIGO_BG,
|
||||
iconBorder: COLOR_INDIGO_BORDER,
|
||||
fields: [
|
||||
{ icon: <IconBuilding />, label: 'Company' },
|
||||
{ icon: <IconUser />, label: 'Users' },
|
||||
{ icon: <IconTarget />, label: 'Opportunity' },
|
||||
],
|
||||
expandCount: 4,
|
||||
x: 280,
|
||||
y: 400,
|
||||
},
|
||||
{
|
||||
id: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
meta: '42',
|
||||
isCustom: false,
|
||||
headerIcon: <IconTargetSm />,
|
||||
iconBg: COLOR_RED_BG,
|
||||
iconBorder: COLOR_RED_BORDER,
|
||||
fields: [
|
||||
{ icon: <IconBuilding />, label: 'Company' },
|
||||
{ icon: <IconTag />, label: '12 fields' },
|
||||
],
|
||||
expandCount: 23,
|
||||
x: 380,
|
||||
y: 190,
|
||||
},
|
||||
];
|
||||
|
||||
export const CONNECTIONS: ConnectionDef[] = [
|
||||
{ from: 'workspaces', to: 'companies' },
|
||||
{ from: 'workspaces', to: 'users' },
|
||||
{ from: 'users', to: 'people' },
|
||||
{ from: 'companies', to: 'people' },
|
||||
{ from: 'companies', to: 'opportunities' },
|
||||
{ from: 'people', to: 'opportunities' },
|
||||
];
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { LayoutFieldIconType } from '../icons/LayoutIcons';
|
||||
|
||||
export type FieldDef = {
|
||||
icon: LayoutFieldIconType;
|
||||
id: string;
|
||||
label: string;
|
||||
section: string;
|
||||
type: string;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export const FIELDS: FieldDef[] = [
|
||||
{
|
||||
id: 'url',
|
||||
icon: 'link',
|
||||
label: 'URL',
|
||||
type: 'Link',
|
||||
section: 'General',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'account-owner',
|
||||
icon: 'user',
|
||||
label: 'Account Owner',
|
||||
type: 'Relation',
|
||||
section: 'General',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
icon: 'money',
|
||||
label: 'Revenue',
|
||||
type: 'Currency',
|
||||
section: 'General',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'icp',
|
||||
icon: 'target',
|
||||
label: 'ICP',
|
||||
type: 'Boolean',
|
||||
section: 'Additional',
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
id: 'employees',
|
||||
icon: 'users',
|
||||
label: 'Employees',
|
||||
type: 'Number',
|
||||
section: 'Other',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'address',
|
||||
icon: 'map',
|
||||
label: 'Address',
|
||||
type: 'Address',
|
||||
section: 'Other',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'creation-date',
|
||||
icon: 'calendar',
|
||||
label: 'Creation date',
|
||||
type: 'Date & Time',
|
||||
section: 'Other',
|
||||
visible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const NAV = [
|
||||
{ icon: 'building', label: 'Companies', active: true, bg: '#d9e2fc' },
|
||||
{ icon: 'user', label: 'People', active: false, bg: '#d9e2fc' },
|
||||
{ icon: 'target', label: 'Opportunities', active: false, bg: '#fdd8d8' },
|
||||
{ icon: 'checkbox', label: 'Tasks', active: false, bg: '#c7ebe5' },
|
||||
{ icon: 'notes', label: 'Notes', active: false, bg: '#c7ebe5' },
|
||||
{
|
||||
icon: 'letter-S',
|
||||
label: 'Sales Dashboard',
|
||||
active: false,
|
||||
bg: '#fef2a4',
|
||||
suffix: 'Dashboard',
|
||||
},
|
||||
{
|
||||
icon: 'automation',
|
||||
label: 'Workflows',
|
||||
active: false,
|
||||
bg: '#ffdcc3',
|
||||
folder: true,
|
||||
children: [
|
||||
{ icon: 'automation', label: 'Workflows', bg: '#ebebeb' },
|
||||
{ icon: 'play', label: 'Workflows runs', bg: '#ebebeb' },
|
||||
{ icon: 'versions', label: 'Workflows versions', bg: '#ebebeb' },
|
||||
],
|
||||
},
|
||||
{ icon: 'ai', label: 'Claude', active: false, bg: '#ebebeb' },
|
||||
{
|
||||
icon: 'stripe-S',
|
||||
label: 'Stripe',
|
||||
active: false,
|
||||
bg: '#ebebeb',
|
||||
folder: true,
|
||||
},
|
||||
] as const;
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { WorkflowIconName } from '../icons/WorkflowIcons';
|
||||
|
||||
export const COLOR_GREEN = '#30a46c';
|
||||
const COLOR_AMBER = '#946800';
|
||||
export const COLOR_GRAY = '#999';
|
||||
|
||||
export const COLOR_TEAL_BG = '#e7f9f5';
|
||||
export const COLOR_GRAY_BG = '#f9f9f9';
|
||||
|
||||
export type NodeDef = {
|
||||
badge?: string;
|
||||
dimmed?: boolean;
|
||||
icon: WorkflowIconName;
|
||||
id: string;
|
||||
label: string;
|
||||
labelColor: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type EdgeDef = {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
const TRUNK_X = 55;
|
||||
const RIGHT_X = 200;
|
||||
const LEFT_X = 5;
|
||||
|
||||
export const NODES: NodeDef[] = [
|
||||
{
|
||||
id: 'trigger',
|
||||
type: 'Trigger',
|
||||
label: 'Record is Created',
|
||||
icon: 'playlist-add',
|
||||
labelColor: COLOR_GREEN,
|
||||
x: TRUNK_X,
|
||||
y: 16,
|
||||
badge: '1',
|
||||
},
|
||||
{
|
||||
id: 'search',
|
||||
type: 'Action',
|
||||
label: 'Search Records',
|
||||
icon: 'search',
|
||||
labelColor: COLOR_GREEN,
|
||||
x: TRUNK_X,
|
||||
y: 92,
|
||||
badge: '1',
|
||||
},
|
||||
{
|
||||
id: 'iterator',
|
||||
type: 'Flow',
|
||||
label: 'Iterator',
|
||||
icon: 'repeat',
|
||||
labelColor: COLOR_AMBER,
|
||||
x: TRUNK_X,
|
||||
y: 168,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
type: 'Action',
|
||||
label: 'Send Email',
|
||||
icon: 'send',
|
||||
labelColor: COLOR_AMBER,
|
||||
x: RIGHT_X,
|
||||
y: 280,
|
||||
},
|
||||
{
|
||||
id: 'update',
|
||||
type: 'Action',
|
||||
label: 'Update Record',
|
||||
icon: 'reload',
|
||||
labelColor: COLOR_GRAY,
|
||||
x: LEFT_X,
|
||||
y: 340,
|
||||
badge: '3',
|
||||
dimmed: true,
|
||||
},
|
||||
{
|
||||
id: 'create',
|
||||
type: 'Action',
|
||||
label: 'Create Record',
|
||||
icon: 'plus',
|
||||
labelColor: COLOR_GREEN,
|
||||
x: RIGHT_X - 5,
|
||||
y: 400,
|
||||
badge: '1',
|
||||
},
|
||||
];
|
||||
|
||||
export const EDGES: EdgeDef[] = [
|
||||
{ from: 'trigger', to: 'search' },
|
||||
{ from: 'search', to: 'iterator' },
|
||||
{ from: 'iterator', to: 'update' },
|
||||
{ from: 'email', to: 'create' },
|
||||
];
|
||||
|
||||
export const ANIMATION_SEQUENCE = [
|
||||
'trigger',
|
||||
'search',
|
||||
'iterator',
|
||||
'email',
|
||||
'update',
|
||||
'create',
|
||||
];
|
||||
|
||||
export const STEP_INTERVAL_MS = 800;
|
||||
export const NODE_WIDTH = 170;
|
||||
export const NODE_HEIGHT = 48;
|
||||
@@ -0,0 +1,196 @@
|
||||
export const IconBuilding = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={12}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
>
|
||||
<path d="M3 21l18 0" />
|
||||
<path d="M5 21v-14l8 -4v18" />
|
||||
<path d="M19 21v-10l-6 -4" />
|
||||
<path d="M9 9l0 .01" />
|
||||
<path d="M9 12l0 .01" />
|
||||
<path d="M9 15l0 .01" />
|
||||
<path d="M9 18l0 .01" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconUser = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={12}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
>
|
||||
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
|
||||
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconApps = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={12}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
>
|
||||
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||
<path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||
<path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||
<path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconTag = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={12}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
>
|
||||
<path d="M7.5 7.5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M3 6v5.172a2 2 0 0 0 .586 1.414l7.71 7.71a2.41 2.41 0 0 0 3.408 0l5.592 -5.592a2.41 2.41 0 0 0 0 -3.408l-7.71 -7.71a2 2 0 0 0 -1.414 -.586h-5.172a3 3 0 0 0 -3 3z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconTarget = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={12}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 7a5 5 0 1 0 5 5" />
|
||||
<path d="M13 3.055a9 9 0 1 0 7.941 7.945" />
|
||||
<path d="M15 6v3h3l3 -3h-3v-3z" />
|
||||
<path d="M15 9l-3 3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconChevronDown = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={12}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={12}
|
||||
>
|
||||
<path d="M6 9l6 -6" />
|
||||
<path d="M6 9l-6 -6" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconBuildingSm = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={9}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
width={9}
|
||||
>
|
||||
<path d="M3 21l18 0" />
|
||||
<path d="M5 21v-14l8 -4v18" />
|
||||
<path d="M19 21v-10l-6 -4" />
|
||||
<path d="M9 9l0 .01" />
|
||||
<path d="M9 12l0 .01" />
|
||||
<path d="M9 15l0 .01" />
|
||||
<path d="M9 18l0 .01" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconUsersSm = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={9}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
viewBox="0 0 24 24"
|
||||
width={9}
|
||||
>
|
||||
<path d="M9 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
|
||||
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconTargetSm = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={9}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
width={9}
|
||||
>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M12 7a5 5 0 1 0 5 5" />
|
||||
<path d="M13 3.055a9 9 0 1 0 7.941 7.945" />
|
||||
<path d="M15 6v3h3l3 -3h-3v-3z" />
|
||||
<path d="M15 9l-3 3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconUserScreenSm = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={9}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
viewBox="0 0 24 24"
|
||||
width={9}
|
||||
>
|
||||
<path d="M19.03 17.818a3 3 0 0 0 1.97 -2.818v-8a3 3 0 0 0 -3 -3h-12a3 3 0 0 0 -3 3v8c0 1.317 .85 2.436 2.03 2.84" />
|
||||
<path d="M10 14a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M8 21a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const IconUserSm = () => (
|
||||
<svg
|
||||
fill="none"
|
||||
height={9}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
width={9}
|
||||
>
|
||||
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" />
|
||||
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,450 @@
|
||||
export type LayoutFieldIconType =
|
||||
| 'link'
|
||||
| 'user'
|
||||
| 'money'
|
||||
| 'target'
|
||||
| 'users'
|
||||
| 'map'
|
||||
| 'calendar';
|
||||
|
||||
export function FieldIcon({ type }: { type: LayoutFieldIconType }) {
|
||||
const c = '#666';
|
||||
const z = 10;
|
||||
switch (type) {
|
||||
case 'link':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<path
|
||||
d="M7 9a3.5 3.5 0 005 0l2-2a3.536 3.536 0 00-5-5L8 3m1 4a3.5 3.5 0 00-5 0L2 9a3.536 3.536 0 005 5l1-1"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'user':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<circle cx="8" cy="5.5" r="2.5" stroke={c} strokeWidth={1.2} />
|
||||
<path
|
||||
d="M3.5 13.5c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'money':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<path
|
||||
d="M8 2v12M5 4.5h4.5a1.5 1.5 0 010 3H5.5a1.5 1.5 0 000 3H11"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'target':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<circle cx="8" cy="8" r="6" stroke={c} strokeWidth={1.2} />
|
||||
<circle cx="8" cy="8" r="3" stroke={c} strokeWidth={1.2} />
|
||||
<circle cx="8" cy="8" fill={c} r="1" />
|
||||
</svg>
|
||||
);
|
||||
case 'users':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<circle cx="6" cy="5" r="2" stroke={c} strokeWidth={1.2} />
|
||||
<path
|
||||
d="M2 13c0-2 1.5-3.5 4-3.5s4 1.5 4 3.5"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<circle cx="11" cy="5.5" r="1.5" stroke={c} strokeWidth={1.2} />
|
||||
<path
|
||||
d="M12 9.5c1.5.5 2.5 1.5 2.5 3"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'map':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<path
|
||||
d="M3 4l3.5-1.5 3 1.5L13 2.5v9.5l-3.5 1.5-3-1.5L3 13.5V4z"
|
||||
stroke={c}
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<path d="M6.5 2.5v9M9.5 4v9" stroke={c} strokeWidth={1.2} />
|
||||
</svg>
|
||||
);
|
||||
case 'calendar':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<rect
|
||||
height="10"
|
||||
rx="1.5"
|
||||
stroke={c}
|
||||
strokeWidth={1.2}
|
||||
width="10"
|
||||
x="3"
|
||||
y="3.5"
|
||||
/>
|
||||
<path d="M3 7h10M6 2v2M10 2v2" stroke={c} strokeWidth={1.2} />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function NavSvgIcon({ type }: { type: string }) {
|
||||
const c = '#555';
|
||||
const z = 7;
|
||||
switch (type) {
|
||||
case 'building':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<path
|
||||
d="M5 14V3l6 2v9M5 6H3v8h12V7h-4"
|
||||
stroke={c}
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'user':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<circle cx="8" cy="5" r="2.5" stroke={c} strokeWidth={1.4} />
|
||||
<path
|
||||
d="M3.5 14c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'target':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<circle cx="8" cy="8" r="5" stroke={c} strokeWidth={1.4} />
|
||||
<circle cx="8" cy="8" r="2" stroke={c} strokeWidth={1.4} />
|
||||
<path d="M8 3v2M13 8h-2" stroke={c} strokeWidth={1.4} />
|
||||
</svg>
|
||||
);
|
||||
case 'checkbox':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<rect
|
||||
height="10"
|
||||
rx="2"
|
||||
stroke={c}
|
||||
strokeWidth={1.4}
|
||||
width="10"
|
||||
x="3"
|
||||
y="3"
|
||||
/>
|
||||
<path
|
||||
d="M6 8l1.5 1.5L10 6"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'notes':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<rect
|
||||
height="10"
|
||||
rx="1.5"
|
||||
stroke={c}
|
||||
strokeWidth={1.4}
|
||||
width="8"
|
||||
x="4"
|
||||
y="3"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 6h3M6.5 8.5h3"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'automation':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<circle cx="8" cy="8" r="5" stroke={c} strokeWidth={1.4} />
|
||||
<path
|
||||
d="M8 5v3l2 1"
|
||||
stroke={c}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'play':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<path
|
||||
d="M5 3.5l8 4.5-8 4.5V3.5z"
|
||||
stroke={c}
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.4}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'versions':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<rect
|
||||
height="8"
|
||||
rx="1"
|
||||
stroke={c}
|
||||
strokeWidth={1.4}
|
||||
width="6"
|
||||
x="5"
|
||||
y="4"
|
||||
/>
|
||||
<path d="M3 6v6a1 1 0 001 1h6" stroke={c} strokeWidth={1.4} />
|
||||
</svg>
|
||||
);
|
||||
case 'ai':
|
||||
return (
|
||||
<svg fill="none" height={z} viewBox="0 0 16 16" width={z}>
|
||||
<path
|
||||
d="M8 2l1.5 4L14 8l-4.5 2L8 14l-1.5-4L2 8l4.5-2L8 2z"
|
||||
stroke={c}
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'letter-S':
|
||||
return (
|
||||
<svg height={z} viewBox="0 0 16 16" width={z}>
|
||||
<text
|
||||
fill="#35290f"
|
||||
fontFamily="Inter"
|
||||
fontSize="9"
|
||||
fontWeight="600"
|
||||
textAnchor="middle"
|
||||
x="8"
|
||||
y="12"
|
||||
>
|
||||
S
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
case 'stripe-S':
|
||||
return (
|
||||
<svg height={z} viewBox="0 0 16 16" width={z}>
|
||||
<text
|
||||
fill="#333"
|
||||
fontFamily="Inter"
|
||||
fontSize="9"
|
||||
fontWeight="600"
|
||||
textAnchor="middle"
|
||||
x="8"
|
||||
y="12"
|
||||
>
|
||||
S
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function EyeIcon({ visible }: { visible: boolean }) {
|
||||
if (visible)
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M2 8s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z"
|
||||
stroke="#999"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<circle cx="8" cy="8" r="1.5" stroke="#999" strokeWidth={1.2} />
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M2 8s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z"
|
||||
stroke="#b3b3b3"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
<path
|
||||
d="M3 3l10 10"
|
||||
stroke="#b3b3b3"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChevLeft() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M10 4l-4 4 4 4"
|
||||
stroke="#999"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChevDown() {
|
||||
return (
|
||||
<svg fill="none" height={7} viewBox="0 0 16 16" width={7}>
|
||||
<path
|
||||
d="M4 6l4 4 4-4"
|
||||
stroke="#999"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.3}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function DotsV() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 4 12" width={5}>
|
||||
<circle cx="2" cy="2" fill="#999" r="0.8" />
|
||||
<circle cx="2" cy="6" fill="#999" r="0.8" />
|
||||
<circle cx="2" cy="10" fill="#999" r="0.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function DotsVW() {
|
||||
return (
|
||||
<svg fill="none" height={12} viewBox="0 0 4 12" width={5}>
|
||||
<circle cx="2" cy="2" fill="white" r="1" />
|
||||
<circle cx="2" cy="6" fill="white" r="1" />
|
||||
<circle cx="2" cy="10" fill="white" r="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GripV() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 8 12" width={7}>
|
||||
<circle cx="3" cy="2" fill="#999" r="0.8" />
|
||||
<circle cx="5" cy="2" fill="#999" r="0.8" />
|
||||
<circle cx="3" cy="6" fill="#999" r="0.8" />
|
||||
<circle cx="5" cy="6" fill="#999" r="0.8" />
|
||||
<circle cx="3" cy="10" fill="#999" r="0.8" />
|
||||
<circle cx="5" cy="10" fill="#999" r="0.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaintSvg() {
|
||||
return (
|
||||
<svg fill="none" height={12} viewBox="0 0 16 16" width={12}>
|
||||
<rect
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke="white"
|
||||
strokeWidth={1.2}
|
||||
width="8"
|
||||
x="4"
|
||||
y="3"
|
||||
/>
|
||||
<path
|
||||
d="M6 9v3a1 1 0 001 1h0a1 1 0 001-1V9"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckSvg() {
|
||||
return (
|
||||
<svg fill="none" height={8} viewBox="0 0 16 16" width={8}>
|
||||
<path
|
||||
d="M3 8l4 4 6-8"
|
||||
stroke="white"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ListSvg() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M3 4h4M3 8h4M3 12h4M9 4h4M9 8h4M9 12h4"
|
||||
stroke="#666"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SparkSvg() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M8 2l1.5 4.5L14 8l-4.5 1.5L8 14l-1.5-4.5L2 8l4.5-1.5L8 2z"
|
||||
stroke="#999"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewSecSvg() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M3 4h10M3 8h5M3 12h10M12 8v4M10 10h4"
|
||||
stroke="#666"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlusSvg() {
|
||||
return (
|
||||
<svg fill="none" height={10} viewBox="0 0 16 16" width={10}>
|
||||
<path
|
||||
d="M8 3v10M3 8h10"
|
||||
stroke="#666"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={1.2}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
export type WorkflowIconName =
|
||||
| 'playlist-add'
|
||||
| 'search'
|
||||
| 'repeat'
|
||||
| 'send'
|
||||
| 'reload'
|
||||
| 'plus';
|
||||
|
||||
const COLOR_AMBER = '#946800';
|
||||
|
||||
export function NodeIcon({ name }: { name: WorkflowIconName }) {
|
||||
switch (name) {
|
||||
case 'playlist-add':
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={16}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
>
|
||||
<path d="M19 8h-14" />
|
||||
<path d="M5 12h9" />
|
||||
<path d="M11 16h-6" />
|
||||
<path d="M15 16h6" />
|
||||
<path d="M18 13v6" />
|
||||
</svg>
|
||||
);
|
||||
case 'search':
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={16}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
|
||||
<path d="M21 21l-6 -6" />
|
||||
</svg>
|
||||
);
|
||||
case 'repeat':
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={16}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
>
|
||||
<path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3" />
|
||||
<path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3" />
|
||||
</svg>
|
||||
);
|
||||
case 'send':
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={16}
|
||||
stroke={COLOR_AMBER}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
>
|
||||
<path d="M10 14l11 -11" />
|
||||
<path d="M21 3l-6.5 18a.55 .55 0 0 1 -1 0l-3.5 -7l-7 -3.5a.55 .55 0 0 1 0 -1l18 -6.5" />
|
||||
</svg>
|
||||
);
|
||||
case 'reload':
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={16}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
>
|
||||
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1.002 7.935 1.007 9.425 4.747" />
|
||||
<path d="M20 4v5h-5" />
|
||||
</svg>
|
||||
);
|
||||
case 'plus':
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height={16}
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
>
|
||||
<path d="M12 5v14" />
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function CheckIcon({ color }: { color: string }) {
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
height="8"
|
||||
stroke={color}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2.5"
|
||||
viewBox="0 0 24 24"
|
||||
width="8"
|
||||
>
|
||||
<path d="M5 12l5 5l10 -10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import {
|
||||
STEPPER_BORDER_LIGHT,
|
||||
STEPPER_BORDER_MEDIUM,
|
||||
STEPPER_BORDER_STRONG,
|
||||
STEPPER_BORDER_SUBTLE,
|
||||
STEPPER_FONT,
|
||||
STEPPER_TEXT,
|
||||
STEPPER_TEXT_SECONDARY,
|
||||
STEPPER_TEXT_TERTIARY,
|
||||
STEPPER_TINT,
|
||||
} from './stepper-visual-tokens';
|
||||
|
||||
const ACCENT = '#3e63dd';
|
||||
const GLASS = 'rgba(255,255,255,0.9)';
|
||||
const R = '3px';
|
||||
|
||||
export const Canvas = styled.div`
|
||||
font-family: ${STEPPER_FONT};
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MainCard = styled.div`
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
height: 84%;
|
||||
left: 16%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 8%;
|
||||
width: 72%;
|
||||
`;
|
||||
|
||||
export const BlueHeader = styled.div`
|
||||
align-items: center;
|
||||
background: ${ACCENT};
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const HeaderLeft = styled.span`
|
||||
color: white;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const HeaderCenter = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
export const HeaderTitle = styled.span`
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const HeaderSave = styled.span`
|
||||
align-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||
border-radius: ${R};
|
||||
color: white;
|
||||
display: flex;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
gap: 3px;
|
||||
padding: 2px 8px;
|
||||
`;
|
||||
|
||||
export const WidgetPanel = styled.div`
|
||||
backdrop-filter: blur(5px);
|
||||
background: ${GLASS};
|
||||
border: 0.8px solid ${ACCENT};
|
||||
border-radius: ${R};
|
||||
left: 50%;
|
||||
max-height: 36%;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
width: 38%;
|
||||
`;
|
||||
|
||||
export const WidgetInner = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
`;
|
||||
|
||||
export const WidgetTitle = styled.div`
|
||||
color: ${STEPPER_TEXT};
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 3px;
|
||||
`;
|
||||
|
||||
export const WSectionLabel = styled.div`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 7px;
|
||||
font-weight: 600;
|
||||
padding: 0 3px;
|
||||
`;
|
||||
|
||||
export const WRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
min-height: 16px;
|
||||
padding: 1px 3px;
|
||||
`;
|
||||
|
||||
export const WIcon = styled.span`
|
||||
align-items: center;
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 10px;
|
||||
justify-content: center;
|
||||
width: 10px;
|
||||
`;
|
||||
|
||||
export const WLabel = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
flex-shrink: 0;
|
||||
font-size: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 58px;
|
||||
`;
|
||||
|
||||
export const WValue = styled.span`
|
||||
color: ${STEPPER_TEXT};
|
||||
flex: 1;
|
||||
font-size: 8px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const WChip = styled.span`
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 0.5px solid ${STEPPER_BORDER_STRONG};
|
||||
border-radius: 50px;
|
||||
color: ${STEPPER_TEXT};
|
||||
font-size: 8px;
|
||||
padding: 1px 6px;
|
||||
`;
|
||||
|
||||
export const NavPanel = styled.div`
|
||||
backdrop-filter: blur(5px);
|
||||
background: ${GLASS};
|
||||
border: 0.8px solid ${ACCENT};
|
||||
border-radius: ${R};
|
||||
left: 10%;
|
||||
max-height: 68%;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
top: 17%;
|
||||
width: 30%;
|
||||
`;
|
||||
|
||||
export const NavSectionLabel = styled.div`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 8px;
|
||||
font-weight: 600;
|
||||
padding: 4px 3px;
|
||||
`;
|
||||
|
||||
export const NavItem = styled.div<{ $active: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ $active }) => ($active ? STEPPER_TINT : 'transparent')};
|
||||
border-radius: ${R};
|
||||
color: ${({ $active }) => ($active ? STEPPER_TEXT : STEPPER_TEXT_SECONDARY)};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
gap: 5px;
|
||||
padding: 5px 4px;
|
||||
`;
|
||||
|
||||
export const NavIconBox = styled.span<{ $bg: string }>`
|
||||
align-items: center;
|
||||
background: ${({ $bg }) => $bg};
|
||||
border: 0.5px solid ${STEPPER_BORDER_STRONG};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 13px;
|
||||
justify-content: center;
|
||||
width: 13px;
|
||||
`;
|
||||
|
||||
export const NavSubItem = styled.div`
|
||||
align-items: center;
|
||||
color: ${STEPPER_TEXT_SECONDARY};
|
||||
display: flex;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
gap: 4px;
|
||||
padding: 4px 4px 4px 16px;
|
||||
`;
|
||||
|
||||
export const NavBreadcrumb = styled.span`
|
||||
border-bottom: 0.5px solid ${STEPPER_BORDER_STRONG};
|
||||
border-left: 0.5px solid ${STEPPER_BORDER_STRONG};
|
||||
border-radius: 0 0 0 2px;
|
||||
flex-shrink: 0;
|
||||
height: 8px;
|
||||
margin-left: -6px;
|
||||
width: 4px;
|
||||
`;
|
||||
|
||||
export const NavSuffix = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 6px;
|
||||
`;
|
||||
|
||||
export const NavChevron = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 6px;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const ActionsBar = styled.div`
|
||||
backdrop-filter: blur(5px);
|
||||
background: ${GLASS};
|
||||
border: 0.8px solid ${ACCENT};
|
||||
border-radius: ${R};
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
left: 54%;
|
||||
padding: 5px 6px;
|
||||
position: absolute;
|
||||
top: 14%;
|
||||
z-index: 3;
|
||||
`;
|
||||
|
||||
export const ActionBtn = styled.span`
|
||||
border: 0.8px solid ${STEPPER_BORDER_SUBTLE};
|
||||
border-radius: ${R};
|
||||
color: ${STEPPER_TEXT_SECONDARY};
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
padding: 3px 6px;
|
||||
`;
|
||||
|
||||
export const RightPanel = styled.div`
|
||||
backdrop-filter: blur(5px);
|
||||
background: ${GLASS};
|
||||
border: 0.8px solid #4a38f5;
|
||||
border-radius: ${R};
|
||||
bottom: 3%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 42%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
width: 54%;
|
||||
z-index: 4;
|
||||
`;
|
||||
|
||||
export const RPHeader = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 0.8px solid ${STEPPER_BORDER_MEDIUM};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 2px;
|
||||
padding: 6px;
|
||||
`;
|
||||
|
||||
export const RPBackBtn = styled.span`
|
||||
align-items: center;
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
export const RPIconBox = styled.span`
|
||||
align-items: center;
|
||||
background: ${STEPPER_TINT};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
export const RPTitleGroup = styled.div`
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
export const RPTitleBold = styled.span`
|
||||
color: ${STEPPER_TEXT};
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const RPTitleSub = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const RPSubBar = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 0.8px solid ${STEPPER_BORDER_LIGHT};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 3px;
|
||||
padding: 5px 6px;
|
||||
`;
|
||||
|
||||
export const RPSubLabel = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const RPFields = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
`;
|
||||
|
||||
export const RPSectionRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 3px;
|
||||
`;
|
||||
|
||||
export const RPSectionName = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
flex: 1;
|
||||
font-size: 7px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const RPEditable = styled.div`
|
||||
align-items: center;
|
||||
background: white;
|
||||
border: 1px solid ${ACCENT};
|
||||
border-radius: ${R};
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin: 2px 0 4px;
|
||||
padding: 4px 6px;
|
||||
`;
|
||||
|
||||
export const RPEditText = styled.span`
|
||||
color: ${STEPPER_TEXT};
|
||||
flex: 1;
|
||||
font-size: 9px;
|
||||
`;
|
||||
|
||||
export const RPDoneBtn = styled.span`
|
||||
background: ${ACCENT};
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 7px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
`;
|
||||
|
||||
export const RPFieldRow = styled.div<{ $dragging: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ $dragging }) =>
|
||||
$dragging ? 'rgba(59,130,246,0.04)' : 'transparent'};
|
||||
border-radius: ${R};
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 3px;
|
||||
touch-action: none;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RPFieldIconBox = styled.span`
|
||||
align-items: center;
|
||||
background: ${STEPPER_TINT};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
export const RPFieldLabels = styled.div`
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: 8px;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const RPFieldName = styled.span`
|
||||
color: ${STEPPER_TEXT_SECONDARY};
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export const RPFieldDot = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
`;
|
||||
|
||||
export const RPFieldType = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
export const RPActionBtn = styled.span`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const RPAddSection = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
padding: 3px;
|
||||
`;
|
||||
|
||||
export const RPAddIconBox = styled.span`
|
||||
align-items: center;
|
||||
background: ${STEPPER_TINT};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
export const RPAddText = styled.span`
|
||||
color: ${STEPPER_TEXT_SECONDARY};
|
||||
font-size: 8px;
|
||||
`;
|
||||
|
||||
export const RPNewFields = styled.div`
|
||||
align-items: center;
|
||||
border-top: 0.8px solid ${STEPPER_BORDER_MEDIUM};
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-top: 4px;
|
||||
padding: 6px 3px 3px;
|
||||
`;
|
||||
|
||||
export const RPNewTitle = styled.span`
|
||||
color: ${STEPPER_TEXT_SECONDARY};
|
||||
font-size: 8px;
|
||||
`;
|
||||
|
||||
export const RPNewDesc = styled.span`
|
||||
color: ${STEPPER_TEXT_TERTIARY};
|
||||
font-size: 7px;
|
||||
`;
|
||||
@@ -0,0 +1,19 @@
|
||||
export const STEPPER_BG = '#ffffff';
|
||||
export const STEPPER_CARD_BG = '#fcfcfc';
|
||||
|
||||
export const STEPPER_TEXT = '#333';
|
||||
export const STEPPER_TEXT_SECONDARY = '#6b7280';
|
||||
export const STEPPER_TEXT_TERTIARY = '#9ca3af';
|
||||
export const STEPPER_TEXT_MUTED = '#666';
|
||||
|
||||
export const STEPPER_BORDER_MEDIUM = '#ebebeb';
|
||||
export const STEPPER_BORDER_STRONG = '#d6d6d6';
|
||||
export const STEPPER_BORDER_LIGHT = '#f1f1f1';
|
||||
export const STEPPER_BORDER_SUBTLE = 'rgba(0, 0, 0, 0.08)';
|
||||
|
||||
export const STEPPER_FONT = "'Inter', sans-serif";
|
||||
export const STEPPER_SHADOW_SM = '0 1px 6px rgba(0, 0, 0, 0.05)';
|
||||
export const STEPPER_TINT = 'rgba(0, 0, 0, 0.04)';
|
||||
|
||||
export const STEPPER_HEADER_BG = '#d9e2fc';
|
||||
export const STEPPER_HEADER_BORDER = '#c6d4f9';
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { ANIMATION_SEQUENCE, STEP_INTERVAL_MS } from './data/workflow.data';
|
||||
|
||||
export function useWorkflowAnimation(active: boolean) {
|
||||
const [activeNodes, setActiveNodes] = useState<Set<string>>(new Set());
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const stepRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
setActiveNodes(new Set());
|
||||
stepRef.current = 0;
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
stepRef.current += 1;
|
||||
if (stepRef.current > ANIMATION_SEQUENCE.length) {
|
||||
stepRef.current = 0;
|
||||
setActiveNodes(new Set());
|
||||
} else {
|
||||
setActiveNodes(new Set(ANIMATION_SEQUENCE.slice(0, stepRef.current)));
|
||||
}
|
||||
}, STEP_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
return activeNodes;
|
||||
}
|
||||
Reference in New Issue
Block a user