diff --git a/packages/twenty-website-new/public/images/product/stepper/step-one.webp b/packages/twenty-website-new/public/images/product/stepper/step-one.webp deleted file mode 100644 index a4df8bae13..0000000000 Binary files a/packages/twenty-website-new/public/images/product/stepper/step-one.webp and /dev/null differ diff --git a/packages/twenty-website-new/public/images/product/stepper/step-three.webp b/packages/twenty-website-new/public/images/product/stepper/step-three.webp deleted file mode 100644 index 037eb1d62d..0000000000 Binary files a/packages/twenty-website-new/public/images/product/stepper/step-three.webp and /dev/null differ diff --git a/packages/twenty-website-new/public/images/product/stepper/step-two.webp b/packages/twenty-website-new/public/images/product/stepper/step-two.webp deleted file mode 100644 index b7b9ab0454..0000000000 Binary files a/packages/twenty-website-new/public/images/product/stepper/step-two.webp and /dev/null differ diff --git a/packages/twenty-website-new/src/app/[locale]/product/page.tsx b/packages/twenty-website-new/src/app/[locale]/product/page.tsx index fb53bd5e9d..f0cf367e96 100644 --- a/packages/twenty-website-new/src/app/[locale]/product/page.tsx +++ b/packages/twenty-website-new/src/app/[locale]/product/page.tsx @@ -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) { {i18n._(msg`Data model`)} ), 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) { {i18n._(msg`Automation`)} ), 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) { {i18n._(msg`Layout`)} ), 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, }, ]; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/Flow.tsx b/packages/twenty-website-new/src/sections/ProductStepper/Flow.tsx index 0e22ea9f36..86df9356f3 100644 --- a/packages/twenty-website-new/src/sections/ProductStepper/Flow.tsx +++ b/packages/twenty-website-new/src/sections/ProductStepper/Flow.tsx @@ -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 ( @@ -86,7 +86,7 @@ export function Flow({ body, children, eyebrow, steps }: FlowProps) { onMobileStepIndexChange={setMobileStepIndex} steps={contentSteps} /> - + ); diff --git a/packages/twenty-website-new/src/sections/ProductStepper/Visual.tsx b/packages/twenty-website-new/src/sections/ProductStepper/Visual.tsx index 7b1c432fe0..f60fb003e5 100644 --- a/packages/twenty-website-new/src/sections/ProductStepper/Visual.tsx +++ b/packages/twenty-website-new/src/sections/ProductStepper/Visual.tsx @@ -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; +}; + 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 ( - +
+
+ +
+
); })} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/types.ts b/packages/twenty-website-new/src/sections/ProductStepper/types.ts index 8ab135c268..11a670eb7c 100644 --- a/packages/twenty-website-new/src/sections/ProductStepper/types.ts +++ b/packages/twenty-website-new/src/sections/ProductStepper/types.ts @@ -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; }; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/AppPreviewShell.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/AppPreviewShell.tsx new file mode 100644 index 0000000000..cdea9e8eba --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/AppPreviewShell.tsx @@ -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 ( + +
+ + + + + + + + + + {title} + + + + + + + + + + + + + + + + + + ⌘K + + +
+ {children} +
+ ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/DataModelVisual.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/DataModelVisual.tsx new file mode 100644 index 0000000000..f978c9a947 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/DataModelVisual.tsx @@ -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, + 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 + >(() => + Object.fromEntries( + ENTITIES.map((entity) => [entity.id, { x: entity.x, y: entity.y }]), + ), + ); + const [hoveredEntity, setHoveredEntity] = useState(null); + const [dragging, setDragging] = useState(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 ( + + + + {CONNECTIONS.map((conn) => ( + + ))} + + + {ENTITIES.map((entity) => { + const pos = positions[entity.id]; + const isHovered = hoveredEntity === entity.id; + return ( + 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', + }} + > + + + {entity.headerIcon} + + {entity.label} + · {entity.meta} + + + L + + + {entity.isCustom ? 'Custom' : 'Standard'} + + + + + {entity.fields.map((field) => ( + + {field.icon} + {field.label} + + ))} + + {entity.expandCount} fields + + + + ); + })} + + + ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/DrawEdge.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/DrawEdge.tsx new file mode 100644 index 0000000000..4dcfb96ebc --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/DrawEdge.tsx @@ -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 ( + + + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/LayoutVisual.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/LayoutVisual.tsx new file mode 100644 index 0000000000..8c9f68d998 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/LayoutVisual.tsx @@ -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(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 ( + + + + + + + + + Layout edition + + + Save + + + + + + + Widget name + General + + + + + URL + qonto.com + + + + + + Account O... + Phil Schiller + + + + + + Address + 18 Rue De Navarin, 750... + + + + + + ICP + ✓ True + + + + + + Workspace + {NAV.map((item) => ( +
+ + + + + {item.label} + {'suffix' in item && item.suffix && ( + · {item.suffix} + )} + {'folder' in item && item.folder && } + + {'children' in item && + item.children?.map((child) => ( + + + + + + {child.label} + + ))} +
+ ))} +
+ + + + New record + ✧ Enrich + ✎ Edit actions + + + + + + + + + + + + Fields + Fields widget + + + + + + + + + + + Layout + + + + {sections.map((section, si) => ( +
+ + + {section} + + + + + + {si === 0 && ( + + Industry + Done + + )} + + {fields + .filter((f) => f.section === section) + .map((field) => ( + handleDragStart(field.id, e)} + > + + + + + {field.label} + · + {field.type} + + { + e.stopPropagation(); + toggleVisibility(field.id); + }} + > + + + + + + + ))} + + {si > 0 && si < sections.length - 1 && ( + + + + + Add a Section + + )} +
+ ))} + + + + + +
+ New fields + Default position/visibility for field… +
+
+ + + + + + Add a Section + +
+
+
+ ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/WorkflowVisual.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/WorkflowVisual.tsx new file mode 100644 index 0000000000..4e17a204e6 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/WorkflowVisual.tsx @@ -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 + >(() => + Object.fromEntries( + NODES.map((node) => [node.id, { x: node.x, y: node.y }]), + ), + ); + const [dragging, setDragging] = useState(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 ( + + + + {EDGES.map((edge) => { + const fromPos = positions[edge.from]; + const toPos = positions[edge.to]; + if (!fromPos || !toPos) return null; + return ( + + ); + })} + + {(() => { + 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 ( + + + + + + + ); + })()} + + + {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 ( + 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', + }} + > + + + + + + {node.type} + {node.badge && ( + {node.badge} + )} + {node.badge && ( + + + + )} + + {node.label} + + + ); + })} + + + iteration 2/3 + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/DataModel.data.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/DataModel.data.tsx new file mode 100644 index 0000000000..3fb7628bc4 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/DataModel.data.tsx @@ -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: , + iconBg: COLOR_GREEN_BG, + iconBorder: COLOR_GREEN_BORDER, + fields: [ + { icon: , label: 'Company' }, + { icon: , label: 'Users' }, + ], + expandCount: 21, + x: 40, + y: 40, + }, + { + id: 'companies', + label: 'Companies', + meta: '39', + isCustom: false, + headerIcon: , + iconBg: COLOR_INDIGO_BG, + iconBorder: COLOR_INDIGO_BORDER, + fields: [ + { icon: , label: 'Workspace' }, + { icon: , label: '31 fields' }, + ], + expandCount: 8, + x: 290, + y: 20, + }, + { + id: 'users', + label: 'Users', + meta: '497', + isCustom: true, + headerIcon: , + iconBg: COLOR_PURPLE_BG, + iconBorder: COLOR_PURPLE_BORDER, + fields: [ + { icon: , label: 'People' }, + { icon: , label: 'Workspace' }, + ], + expandCount: 32, + x: 40, + y: 310, + }, + { + id: 'people', + label: 'People', + meta: '648', + isCustom: false, + headerIcon: , + iconBg: COLOR_INDIGO_BG, + iconBorder: COLOR_INDIGO_BORDER, + fields: [ + { icon: , label: 'Company' }, + { icon: , label: 'Users' }, + { icon: , label: 'Opportunity' }, + ], + expandCount: 4, + x: 280, + y: 400, + }, + { + id: 'opportunities', + label: 'Opportunities', + meta: '42', + isCustom: false, + headerIcon: , + iconBg: COLOR_RED_BG, + iconBorder: COLOR_RED_BORDER, + fields: [ + { icon: , label: 'Company' }, + { icon: , 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' }, +]; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/layout.data.ts b/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/layout.data.ts new file mode 100644 index 0000000000..cccb145673 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/layout.data.ts @@ -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; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/workflow.data.ts b/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/workflow.data.ts new file mode 100644 index 0000000000..7acea0e718 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/data/workflow.data.ts @@ -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; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/DataModelIcons.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/DataModelIcons.tsx new file mode 100644 index 0000000000..2b75639f73 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/DataModelIcons.tsx @@ -0,0 +1,196 @@ +export const IconBuilding = () => ( + + + + + + + + + +); + +export const IconUser = () => ( + + + + +); + +export const IconApps = () => ( + + + + + + +); + +export const IconTag = () => ( + + + + +); + +export const IconTarget = () => ( + + + + + + + +); + +export const IconChevronDown = () => ( + + + + +); + +export const IconBuildingSm = () => ( + + + + + + + + + +); + +export const IconUsersSm = () => ( + + + + + + +); + +export const IconTargetSm = () => ( + + + + + + + +); + +export const IconUserScreenSm = () => ( + + + + + +); + +export const IconUserSm = () => ( + + + + +); diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/LayoutIcons.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/LayoutIcons.tsx new file mode 100644 index 0000000000..4835186fbe --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/LayoutIcons.tsx @@ -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 ( + + + + ); + case 'user': + return ( + + + + + ); + case 'money': + return ( + + + + ); + case 'target': + return ( + + + + + + ); + case 'users': + return ( + + + + + + + ); + case 'map': + return ( + + + + + ); + case 'calendar': + return ( + + + + + ); + default: + return null; + } +} + +export function NavSvgIcon({ type }: { type: string }) { + const c = '#555'; + const z = 7; + switch (type) { + case 'building': + return ( + + + + ); + case 'user': + return ( + + + + + ); + case 'target': + return ( + + + + + + ); + case 'checkbox': + return ( + + + + + ); + case 'notes': + return ( + + + + + ); + case 'automation': + return ( + + + + + ); + case 'play': + return ( + + + + ); + case 'versions': + return ( + + + + + ); + case 'ai': + return ( + + + + ); + case 'letter-S': + return ( + + + S + + + ); + case 'stripe-S': + return ( + + + S + + + ); + default: + return null; + } +} + +export function EyeIcon({ visible }: { visible: boolean }) { + if (visible) + return ( + + + + + ); + return ( + + + + + ); +} + +export function ChevLeft() { + return ( + + + + ); +} + +export function ChevDown() { + return ( + + + + ); +} + +export function DotsV() { + return ( + + + + + + ); +} + +export function DotsVW() { + return ( + + + + + + ); +} + +export function GripV() { + return ( + + + + + + + + + ); +} + +export function PaintSvg() { + return ( + + + + + ); +} + +export function CheckSvg() { + return ( + + + + ); +} + +export function ListSvg() { + return ( + + + + ); +} + +export function SparkSvg() { + return ( + + + + ); +} + +export function NewSecSvg() { + return ( + + + + ); +} + +export function PlusSvg() { + return ( + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/WorkflowIcons.tsx b/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/WorkflowIcons.tsx new file mode 100644 index 0000000000..7cb94340f5 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/icons/WorkflowIcons.tsx @@ -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 ( + + + + + + + + ); + case 'search': + return ( + + + + + ); + case 'repeat': + return ( + + + + + ); + case 'send': + return ( + + + + + ); + case 'reload': + return ( + + + + + ); + case 'plus': + return ( + + + + + ); + } +} + +export function CheckIcon({ color }: { color: string }) { + return ( + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/layout-styles.ts b/packages/twenty-website-new/src/sections/ProductStepper/visuals/layout-styles.ts new file mode 100644 index 0000000000..4d577d792e --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/layout-styles.ts @@ -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; +`; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/stepper-visual-tokens.ts b/packages/twenty-website-new/src/sections/ProductStepper/visuals/stepper-visual-tokens.ts new file mode 100644 index 0000000000..dd06a8c35b --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/stepper-visual-tokens.ts @@ -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'; diff --git a/packages/twenty-website-new/src/sections/ProductStepper/visuals/use-workflow-animation.ts b/packages/twenty-website-new/src/sections/ProductStepper/visuals/use-workflow-animation.ts new file mode 100644 index 0000000000..0e8c43e6c6 --- /dev/null +++ b/packages/twenty-website-new/src/sections/ProductStepper/visuals/use-workflow-animation.ts @@ -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>(new Set()); + const intervalRef = useRef | 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; +}