diff --git a/packages/twenty-website/src/platform/motion/get-element-scale.ts b/packages/twenty-website/src/platform/motion/get-element-scale.ts new file mode 100644 index 0000000000..936beb6b2a --- /dev/null +++ b/packages/twenty-website/src/platform/motion/get-element-scale.ts @@ -0,0 +1,8 @@ +export function getElementScale(element: HTMLElement): number { + const layoutWidth = element.offsetWidth; + + if (layoutWidth === 0) { + return 1; + } + return element.getBoundingClientRect().width / layoutWidth; +} diff --git a/packages/twenty-website/src/platform/motion/index.ts b/packages/twenty-website/src/platform/motion/index.ts index 71304d368f..5da62924b7 100644 --- a/packages/twenty-website/src/platform/motion/index.ts +++ b/packages/twenty-website/src/platform/motion/index.ts @@ -7,6 +7,7 @@ export { type ScrollProgressEffectProps, } from './ScrollProgressEffect'; export { getReducedMotionSnapshot } from './reduced-motion-snapshot'; +export { getElementScale } from './get-element-scale'; export { observeElementSize } from './observe-element-size'; export { useScaleToFit } from './use-scale-to-fit'; export { useMediaQuery } from './use-media-query'; diff --git a/packages/twenty-website/src/platform/motion/use-pointer-drag-positions.ts b/packages/twenty-website/src/platform/motion/use-pointer-drag-positions.ts index ae5605528d..4f376e8ba5 100644 --- a/packages/twenty-website/src/platform/motion/use-pointer-drag-positions.ts +++ b/packages/twenty-website/src/platform/motion/use-pointer-drag-positions.ts @@ -7,6 +7,8 @@ import { type PointerEvent as ReactPointerEvent, } from 'react'; +import { getElementScale } from './get-element-scale'; + type Point = { x: number; y: number }; export type PointerDragPositions = { @@ -44,6 +46,7 @@ export function usePointerDragPositions( id: string; positionX: number; positionY: number; + scale: number; startX: number; startY: number; } | null>(null); @@ -79,6 +82,7 @@ export function usePointerDragPositions( startY: event.clientY, positionX: position.x, positionY: position.y, + scale: getElementScale(event.currentTarget), }; setDraggingId(id); }; @@ -89,9 +93,9 @@ export function usePointerDragPositions( if (!dragStart || event.pointerId !== captureRef.current?.pointerId) { return; } - const { id, startX, startY, positionX, positionY } = dragStart; - const deltaX = event.clientX - startX; - const deltaY = event.clientY - startY; + const { id, startX, startY, positionX, positionY, scale } = dragStart; + const deltaX = (event.clientX - startX) / scale; + const deltaY = (event.clientY - startY) / scale; setPositions((previous) => ({ ...previous, [id]: { x: positionX + deltaX, y: positionY + deltaY }, diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/DataModelVisual.tsx b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/DataModelVisual.tsx new file mode 100644 index 0000000000..21848cdb4b --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/DataModelVisual.tsx @@ -0,0 +1,243 @@ +'use client'; + +import { styled } from '@linaria/react'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { useState } from 'react'; + +import { usePointerDragPositions } from '@/platform/motion'; +import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; + +import { STEPPER_SHELL_CHROME } from '../components/ProductStepperShell'; +import { DATA_MODEL_ICONS } from './components/DataModelIcons'; +import { DrawEdge } from './components/DrawEdge'; +import { DATA_MODEL_GRAPH } from './data/data-model-data'; +import { type EntityConnection } from './types/entity-connection'; + +const shell = PRODUCT_STEPPER_SCENE.shell; +const entityTones = PRODUCT_STEPPER_SCENE.entityTones; + +const { Canvas, Shell, StageFit, SvgLayer } = STEPPER_SHELL_CHROME; + +const EntityCard = styled.div` + backdrop-filter: blur(14px); + background: ${shell.cardBackground}; + border: 1px solid ${shell.borderMedium}; + border-radius: 8px; + box-shadow: ${PRODUCT_STEPPER_SCENE.cardShadow}; + cursor: grab; + display: flex; + flex-direction: column; + gap: 6px; + padding: 6px; + position: absolute; + touch-action: none; + transition: border-color 0.15s ease; + width: 180px; + z-index: 2; + + &[data-hovered] { + border-color: ${shell.borderStrong}; + } + + &: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 ${shell.borderLight}; + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 2px; + overflow: hidden; + padding: 4px 0; +`; + +const EntityIcon = styled.span<{ $tint: string; $line: string }>` + align-items: center; + background: ${({ $tint }) => $tint}; + border: 1px solid ${({ $line }) => $line}; + border-radius: 3px; + color: ${shell.text}; + display: flex; + height: 14px; + justify-content: center; + width: 14px; +`; + +const EntityLabel = styled.span` + color: ${shell.text}; + font-size: 12px; + font-weight: 500; + line-height: 1.4; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +`; + +const EntityMeta = styled.span` + color: ${shell.textTertiary}; + flex-shrink: 0; + font-size: 12px; + font-weight: 500; + line-height: 1.4; +`; + +const FieldRow = styled.div` + align-items: center; + color: ${shell.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; + display: flex; + height: 14px; + justify-content: center; + width: 14px; +`; + +const ExpandHint = styled.div` + align-items: center; + color: ${shell.textTertiary}; + display: flex; + font-size: 12px; + font-weight: 400; + gap: 4px; + height: 22px; + line-height: 1.4; + padding: 0 6px; +`; + +const CARD_WIDTH = 180; +const CARD_HEIGHT_ESTIMATE = 110; + +type EntityPositions = Record; + +function getCardCenter( + positions: EntityPositions, + entityId: string, +): { x: number; y: number } { + const position = positions[entityId]; + + if (!position) { + return { x: 0, y: 0 }; + } + return { + x: position.x + CARD_WIDTH / 2, + y: position.y + CARD_HEIGHT_ESTIMATE / 2, + }; +} + +export function DataModelVisual({ active }: { active: boolean }) { + const { + canvasHandlers, + draggingId: dragging, + handlePointerDown, + positions, + } = usePointerDragPositions(() => + Object.fromEntries( + DATA_MODEL_GRAPH.entities.map((entity) => [ + entity.id, + { x: entity.x, y: entity.y }, + ]), + ), + ); + const [hoveredEntity, setHoveredEntity] = useState(null); + const { i18n } = useLingui(); + + const isConnectionHighlighted = (connection: EntityConnection) => + hoveredEntity === connection.from || hoveredEntity === connection.to; + + return ( + + + + + {DATA_MODEL_GRAPH.connections.map((connection) => ( + + ))} + + + {DATA_MODEL_GRAPH.entities.map((entity) => { + const position = positions[entity.id]; + const HeaderIcon = DATA_MODEL_ICONS.headers[entity.headerIcon]; + const tone = entityTones[entity.tone]; + + return ( + handlePointerDown(entity.id, event)} + onPointerEnter={() => setHoveredEntity(entity.id)} + onPointerLeave={() => setHoveredEntity(null)} + style={{ + left: position.x, + top: position.y, + transition: + dragging === entity.id ? 'none' : 'border-color 0.15s', + }} + > + + + + + {i18n._(entity.label)} + · {entity.meta} + + + {entity.fields.map((field) => { + const Icon = DATA_MODEL_ICONS.fields[field.icon]; + return ( + + + + + {i18n._(field.label)} + + ); + })} + + + + + {entity.expandCount} {i18n._(msg`fields`)} + + + + ); + })} + + + + ); +} diff --git a/packages/twenty-website/src/sections/product-stepper/components/DataModelIcons.tsx b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/components/DataModelIcons.tsx similarity index 90% rename from packages/twenty-website/src/sections/product-stepper/components/DataModelIcons.tsx rename to packages/twenty-website/src/sections/product-stepper/DataModelVisual/components/DataModelIcons.tsx index b207822b5f..33cdd319ad 100644 --- a/packages/twenty-website/src/sections/product-stepper/components/DataModelIcons.tsx +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/components/DataModelIcons.tsx @@ -1,17 +1,3 @@ -export type DataModelFieldIcon = - | 'apps' - | 'building' - | 'tag' - | 'target' - | 'user'; - -export type DataModelHeaderIcon = - | 'buildingSmall' - | 'targetSmall' - | 'userScreenSmall' - | 'userSmall' - | 'usersSmall'; - function IconBuilding() { return ( + + + + + + ); +} + function IconBuildingSmall() { return ( - - + ); } @@ -241,6 +246,7 @@ export const DATA_MODEL_ICONS = { user: IconUser, }, headers: { + briefcaseSmall: IconBriefcaseSmall, buildingSmall: IconBuildingSmall, targetSmall: IconTargetSmall, userScreenSmall: IconUserScreenSmall, diff --git a/packages/twenty-website/src/sections/product-stepper/components/DrawEdge.tsx b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/components/DrawEdge.tsx similarity index 100% rename from packages/twenty-website/src/sections/product-stepper/components/DrawEdge.tsx rename to packages/twenty-website/src/sections/product-stepper/DataModelVisual/components/DrawEdge.tsx diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/data/data-model-data.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/data/data-model-data.ts new file mode 100644 index 0000000000..9e962cab4c --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/data/data-model-data.ts @@ -0,0 +1,93 @@ +import { msg } from '@lingui/core/macro'; + +import { type EntityConnection } from '../types/entity-connection'; +import { type EntityDefinition } from '../types/entity-definition'; + +export const DATA_MODEL_GRAPH: { + connections: EntityConnection[]; + entities: EntityDefinition[]; +} = { + entities: [ + { + id: 'people', + label: msg`People`, + meta: '840', + isCustom: false, + headerIcon: 'userSmall', + tone: 'green', + fields: [ + { id: 'company', icon: 'building', label: msg`Company` }, + { id: 'opportunities', icon: 'target', label: msg`Opportunities` }, + ], + expandCount: 22, + x: 40, + y: 40, + }, + { + id: 'companies', + label: msg`Companies`, + meta: '120', + isCustom: false, + headerIcon: 'buildingSmall', + tone: 'indigo', + fields: [ + { id: 'people', icon: 'user', label: msg`People` }, + { id: 'opportunities', icon: 'target', label: msg`Opportunities` }, + ], + expandCount: 39, + x: 330, + y: 20, + }, + { + id: 'employment-history', + label: msg`Employment History`, + meta: '48', + isCustom: true, + headerIcon: 'briefcaseSmall', + tone: 'purple', + fields: [ + { id: 'company', icon: 'building', label: msg`Company` }, + { id: 'person', icon: 'user', label: msg`Person` }, + ], + expandCount: 8, + x: 40, + y: 310, + }, + { + id: 'opportunities', + label: msg`Opportunities`, + meta: '64', + isCustom: false, + headerIcon: 'targetSmall', + tone: 'red', + fields: [ + { id: 'company', icon: 'building', label: msg`Company` }, + { id: 'point-of-contact', icon: 'user', label: msg`Point of Contact` }, + ], + expandCount: 11, + x: 380, + y: 190, + }, + { + id: 'demos', + label: msg`Demos`, + meta: '45', + isCustom: true, + headerIcon: 'userScreenSmall', + tone: 'indigo', + fields: [ + { id: 'company', icon: 'building', label: msg`Company` }, + { id: 'opportunity', icon: 'target', label: msg`Opportunity` }, + ], + expandCount: 6, + x: 280, + y: 400, + }, + ], + connections: [ + { from: 'people', to: 'companies' }, + { from: 'companies', to: 'opportunities' }, + { from: 'opportunities', to: 'demos' }, + { from: 'people', to: 'employment-history' }, + ], +}; diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/index.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/index.ts new file mode 100644 index 0000000000..73b5247120 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/index.ts @@ -0,0 +1 @@ +export { DataModelVisual } from './DataModelVisual'; diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/data-model-field-icon.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/data-model-field-icon.ts new file mode 100644 index 0000000000..6158473d71 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/data-model-field-icon.ts @@ -0,0 +1,6 @@ +export type DataModelFieldIcon = + | 'apps' + | 'building' + | 'tag' + | 'target' + | 'user'; diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/data-model-header-icon.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/data-model-header-icon.ts new file mode 100644 index 0000000000..0896ad409f --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/data-model-header-icon.ts @@ -0,0 +1,7 @@ +export type DataModelHeaderIcon = + | 'briefcaseSmall' + | 'buildingSmall' + | 'targetSmall' + | 'userScreenSmall' + | 'userSmall' + | 'usersSmall'; diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-connection.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-connection.ts new file mode 100644 index 0000000000..e15830b65a --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-connection.ts @@ -0,0 +1,4 @@ +export type EntityConnection = { + from: string; + to: string; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-definition.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-definition.ts new file mode 100644 index 0000000000..7ade3d1482 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-definition.ts @@ -0,0 +1,18 @@ +import { type MessageDescriptor } from '@lingui/core'; + +import { type DataModelFieldIcon } from './data-model-field-icon'; +import { type DataModelHeaderIcon } from './data-model-header-icon'; +import { type EntityTone } from './entity-tone'; + +export type EntityDefinition = { + expandCount: number; + fields: { icon: DataModelFieldIcon; id: string; label: MessageDescriptor }[]; + headerIcon: DataModelHeaderIcon; + id: string; + isCustom: boolean; + label: MessageDescriptor; + meta: string; + tone: EntityTone; + x: number; + y: number; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-tone.ts b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-tone.ts new file mode 100644 index 0000000000..2fa372d14e --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/DataModelVisual/types/entity-tone.ts @@ -0,0 +1,3 @@ +import { type PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; + +export type EntityTone = keyof typeof PRODUCT_STEPPER_SCENE.entityTones; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/LayoutVisual.tsx b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/LayoutVisual.tsx new file mode 100644 index 0000000000..966a6b0e46 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/LayoutVisual.tsx @@ -0,0 +1,354 @@ +'use client'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { + useRef, + useState, + type PointerEvent as ReactPointerEvent, +} from 'react'; + +import { getElementScale } from '@/platform/motion'; +import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; + +import { STEPPER_SHELL_CHROME } from '../components/ProductStepperShell'; +import { LAYOUT_GLYPHS } from './components/LayoutIcons'; +import { LAYOUT_CHROME } from './components/layout-chrome'; +import { LAYOUT_EDITOR_CONTENT } from './data/layout-data'; + +const { + ChevronLeft: ChevronLeftGlyph, + Dots: DotsGlyph, + Eye: EyeGlyph, + Field: FieldGlyph, + Grip: GripGlyph, + List: ListGlyph, + Nav: NavGlyph, + NewSection: NewSectionGlyph, + Plus: PlusGlyph, + Spark: SparkGlyph, +} = LAYOUT_GLYPHS; + +const { + ActionButton, + ActionsBar, + AddIconBox, + AddSectionRow, + AddText, + DoneButton, + EditableRow, + EditableText, + FieldDot, + FieldIconBox, + FieldLabels, + FieldName, + FieldRow, + FieldType, + NavBreadcrumb, + NavChevron, + NavIconBox, + NavItem, + NavPanel, + NavSectionLabel, + NavSubItem, + NewFieldsColumn, + NewFieldsDescription, + NewFieldsRow, + NewFieldsTitle, + PanelActionButton, + PanelBackButton, + PanelFields, + PanelHeader, + PanelIconBox, + PanelSubBar, + PanelSubLabel, + PanelTitleBold, + PanelTitleGroup, + PanelTitleSub, + RightPanel, + SectionName, + SectionRow, + WidgetChip, + WidgetIcon, + WidgetInner, + WidgetLabel, + WidgetPanel, + WidgetRow, + WidgetSectionLabel, + WidgetValue, +} = LAYOUT_CHROME; + +const { Canvas, Shell, StageFit } = STEPPER_SHELL_CHROME; + +const LAYOUT_DESIGN = { height: 496, width: 473 }; + +const ROW_STEP_PX = 22; + +export function LayoutVisual({ active }: { active: boolean }) { + const [fields, setFields] = useState(LAYOUT_EDITOR_CONTENT.fields); + const [draggingId, setDraggingId] = useState(null); + const dragStartY = useRef(0); + const dragScale = useRef(1); + const { i18n } = useLingui(); + + const toggleVisibility = (fieldId: string) => { + setFields((previous) => + previous.map((field) => + field.id === fieldId ? { ...field, visible: !field.visible } : field, + ), + ); + }; + + const handleDragStart = ( + fieldId: string, + event: ReactPointerEvent, + ) => { + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + setDraggingId(fieldId); + dragStartY.current = event.clientY; + dragScale.current = getElementScale(event.currentTarget); + }; + + const handleDragMove = (event: ReactPointerEvent) => { + if (!draggingId) { + return; + } + const deltaY = (event.clientY - dragStartY.current) / dragScale.current; + const steps = Math.round(deltaY / ROW_STEP_PX); + + if (steps === 0) { + return; + } + setFields((previous) => { + const index = previous.findIndex((field) => field.id === draggingId); + + if (index === -1) { + return previous; + } + const target = Math.max(0, Math.min(previous.length - 1, index + steps)); + + if (target === index) { + return previous; + } + const next = [...previous]; + const [moved] = next.splice(index, 1); + next.splice(target, 0, moved); + return next; + }); + dragStartY.current = event.clientY; + }; + + const handleDragEnd = () => setDraggingId(null); + const handleLostCapture = () => { + if (draggingId) { + setDraggingId(null); + } + }; + const sections = [...new Set(fields.map((field) => field.section))]; + + return ( + + + + + + {i18n._(msg`General`)} + + + + + {i18n._(msg`URL`)} + anthropic.com + + + + + + {i18n._(msg`Account Owner`)} + Félix Malfait + + + + + + {i18n._(msg`Address`)} + 548 Market St, San Fr... + + + + + + {i18n._(msg`ICP`)} + ✓ True + + + + + + {i18n._(msg`Workspace`)} + {LAYOUT_EDITOR_CONTENT.navItems.map((item) => ( +
+ + + + + {i18n._(item.label)} + {item.isFolder ? : null} + + {item.children?.map((child) => ( + + + + + + {i18n._(child.label)} + + ))} +
+ ))} +
+ + + + {i18n._(msg`New record`)} + ✧ {i18n._(msg`Enrich`)} + ✎ {i18n._(msg`Edit actions`)} + + + + + + + + + + + + {i18n._(msg`Fields`)} + {i18n._(msg`Fields widget`)} + + + + + + + + + + + {i18n._(msg`Layout`)} + + + + {sections.map((section, sectionNumber) => ( +
+ + + + {i18n._(LAYOUT_EDITOR_CONTENT.sectionLabels[section])} + + + + {sectionNumber === 0 ? ( + + {i18n._(msg`Industry`)} + {i18n._(msg`Done`)} + + ) : null} + + {fields + .filter((field) => field.section === section) + .map((field) => ( + + handleDragStart(field.id, event) + } + > + + + + + {i18n._(field.label)} + · + {i18n._(field.type)} + + { + event.stopPropagation(); + toggleVisibility(field.id); + }} + > + + + + + + + ))} + + {sectionNumber > 0 && sectionNumber < sections.length - 1 ? ( + + + + + {i18n._(msg`Add a Section`)} + + ) : null} +
+ ))} + + + + + + + {i18n._(msg`New fields`)} + + {i18n._(msg`Default position/visibility for field…`)} + + + + + + + + + {i18n._(msg`Add a Section`)} + +
+
+
+
+
+ ); +} diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/components/LayoutIcons.tsx b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/components/LayoutIcons.tsx new file mode 100644 index 0000000000..3c4976d69a --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/components/LayoutIcons.tsx @@ -0,0 +1,113 @@ +import { + IconBuildingSkyscraper, + IconCalendar, + IconCheckbox, + IconChevronLeft, + IconCurrencyDollar, + IconDotsVertical, + IconEye, + IconEyeOff, + IconGripVertical, + IconHistoryToggle, + IconLayoutDashboard, + IconLink, + IconList, + IconMapPin, + IconNotes, + IconPlus, + IconSettingsAutomation, + IconSparkles, + IconTargetArrow, + IconUser, + IconUserCircle, + IconUsers, + IconVersions, +} from '@tabler/icons-react'; + +import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; + +import { type LayoutFieldIconType } from '../types/layout-field-icon-type'; +import { type LayoutNavIconType } from '../types/layout-nav-icon-type'; + +const inks = PRODUCT_STEPPER_SCENE.layout; + +const FIELD_ICONS: Record = { + calendar: IconCalendar, + link: IconLink, + map: IconMapPin, + money: IconCurrencyDollar, + target: IconTargetArrow, + user: IconUserCircle, + users: IconUsers, +}; + +const NAV_ICONS: Record = { + automation: IconSettingsAutomation, + building: IconBuildingSkyscraper, + checkbox: IconCheckbox, + dashboard: IconLayoutDashboard, + history: IconHistoryToggle, + notes: IconNotes, + target: IconTargetArrow, + user: IconUser, + versions: IconVersions, +}; + +function FieldGlyph({ type }: { type: LayoutFieldIconType }) { + const IconComponent = FIELD_ICONS[type]; + return ; +} + +function NavGlyph({ color, type }: { color: string; type: LayoutNavIconType }) { + const IconComponent = NAV_ICONS[type]; + return ; +} + +function EyeGlyph({ visible }: { visible: boolean }) { + return visible ? ( + + ) : ( + + ); +} + +function ChevronLeftGlyph() { + return ; +} + +function DotsGlyph() { + return ; +} + +function GripGlyph() { + return ; +} + +function ListGlyph() { + return ; +} + +function SparkGlyph() { + return ; +} + +function NewSectionGlyph() { + return ; +} + +function PlusGlyph() { + return ; +} + +export const LAYOUT_GLYPHS = { + ChevronLeft: ChevronLeftGlyph, + Dots: DotsGlyph, + Eye: EyeGlyph, + Field: FieldGlyph, + Grip: GripGlyph, + List: ListGlyph, + Nav: NavGlyph, + NewSection: NewSectionGlyph, + Plus: PlusGlyph, + Spark: SparkGlyph, +}; diff --git a/packages/twenty-website/src/sections/product-stepper/data/layout-chrome.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/components/layout-chrome.ts similarity index 82% rename from packages/twenty-website/src/sections/product-stepper/data/layout-chrome.ts rename to packages/twenty-website/src/sections/product-stepper/LayoutVisual/components/layout-chrome.ts index 1a634dbd54..46774a4bdd 100644 --- a/packages/twenty-website/src/sections/product-stepper/data/layout-chrome.ts +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/components/layout-chrome.ts @@ -7,80 +7,17 @@ const layout = PRODUCT_STEPPER_SCENE.layout; const PANEL_RADIUS = '3px'; -const Canvas = styled.div` - font-family: ${shell.font}; - height: 100%; - opacity: 0.6; - position: relative; - transition: opacity 0.3s; - width: 100%; - - &[data-active] { - opacity: 1; - } -`; - -const MainCard = styled.div` - background: white; - border-radius: 4px; - height: 84%; - left: 16%; - overflow: hidden; - position: absolute; - top: 8%; - width: 72%; -`; - -const BlueHeader = styled.div` - align-items: center; - background: ${layout.accent}; - display: flex; - height: 32px; - justify-content: space-between; - padding: 0 10px; - width: 100%; -`; - -const HeaderLeft = styled.span` - color: white; - display: flex; -`; - -const HeaderCenter = styled.div` - align-items: center; - display: flex; - gap: 4px; -`; - -const HeaderTitle = styled.span` - color: white; - font-size: 10px; - font-weight: 500; -`; - -const HeaderSave = styled.span` - align-items: center; - border: 1px solid ${layout.saveBorder}; - border-radius: ${PANEL_RADIUS}; - color: white; - display: flex; - font-size: 8px; - font-weight: 500; - gap: 3px; - padding: 2px 8px; -`; - const WidgetPanel = styled.div` backdrop-filter: blur(5px); - background: ${layout.glass}; + background: ${shell.cardBackground}; border: 0.8px solid ${layout.accent}; border-radius: ${PANEL_RADIUS}; - left: 50%; + left: 48%; max-height: 36%; overflow: hidden; padding: 6px; position: absolute; - top: 20%; + top: 13%; width: 38%; `; @@ -90,13 +27,6 @@ const WidgetInner = styled.div` gap: 5px; `; -const WidgetTitle = styled.div` - color: ${shell.text}; - font-size: 10px; - font-weight: 600; - padding: 2px 3px; -`; - const WidgetSectionLabel = styled.div` color: ${shell.textTertiary}; font-size: 7px; @@ -153,15 +83,15 @@ const WidgetChip = styled.span` const NavPanel = styled.div` backdrop-filter: blur(5px); - background: ${layout.glass}; + background: ${shell.cardBackground}; border: 0.8px solid ${layout.accent}; border-radius: ${PANEL_RADIUS}; - left: 10%; + left: 8%; max-height: 68%; overflow-y: auto; padding: 8px; position: absolute; - top: 17%; + top: 13%; width: 30%; `; @@ -190,10 +120,10 @@ const NavItem = styled.div` } `; -const NavIconBox = styled.span<{ $tint: string }>` +const NavIconBox = styled.span<{ $background: string; $border: string }>` align-items: center; - background: ${({ $tint }) => $tint}; - border: 0.5px solid ${shell.borderStrong}; + background: ${({ $background }) => $background}; + border: 0.5px solid ${({ $border }) => $border}; border-radius: 3px; display: flex; flex-shrink: 0; @@ -222,11 +152,6 @@ const NavBreadcrumb = styled.span` width: 4px; `; -const NavSuffix = styled.span` - color: ${shell.textTertiary}; - font-size: 6px; -`; - const NavChevron = styled.span` color: ${shell.textTertiary}; font-size: 6px; @@ -235,15 +160,15 @@ const NavChevron = styled.span` const ActionsBar = styled.div` backdrop-filter: blur(5px); - background: ${layout.glass}; + background: ${shell.cardBackground}; border: 0.8px solid ${layout.accent}; border-radius: ${PANEL_RADIUS}; display: flex; gap: 5px; - left: 54%; + left: 46%; padding: 5px 6px; position: absolute; - top: 14%; + top: 6%; z-index: 3; `; @@ -258,7 +183,7 @@ const ActionButton = styled.span` const RightPanel = styled.div` backdrop-filter: blur(5px); - background: ${layout.glass}; + background: ${shell.cardBackground}; border: 0.8px solid ${layout.panelAccent}; border-radius: ${PANEL_RADIUS}; bottom: 3%; @@ -267,7 +192,7 @@ const RightPanel = styled.div` left: 42%; overflow: hidden; position: absolute; - top: 40%; + top: 36%; width: 54%; z-index: 4; `; @@ -501,8 +426,6 @@ export const LAYOUT_CHROME = { AddIconBox, AddSectionRow, AddText, - BlueHeader, - Canvas, DoneButton, EditableRow, EditableText, @@ -512,11 +435,6 @@ export const LAYOUT_CHROME = { FieldName, FieldRow, FieldType, - HeaderCenter, - HeaderLeft, - HeaderSave, - HeaderTitle, - MainCard, NavBreadcrumb, NavChevron, NavIconBox, @@ -524,7 +442,6 @@ export const LAYOUT_CHROME = { NavPanel, NavSectionLabel, NavSubItem, - NavSuffix, NewFieldsColumn, NewFieldsDescription, NewFieldsRow, @@ -549,6 +466,5 @@ export const LAYOUT_CHROME = { WidgetPanel, WidgetRow, WidgetSectionLabel, - WidgetTitle, WidgetValue, }; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/data/layout-data.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/data/layout-data.ts new file mode 100644 index 0000000000..b81df635f6 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/data/layout-data.ts @@ -0,0 +1,110 @@ +import { type MessageDescriptor } from '@lingui/core'; +import { msg } from '@lingui/core/macro'; + +import { type LayoutFieldDefinition } from '../types/layout-field-definition'; +import { type LayoutNavItemDefinition } from '../types/layout-nav-item-definition'; + +export const LAYOUT_EDITOR_CONTENT: { + fields: LayoutFieldDefinition[]; + navItems: LayoutNavItemDefinition[]; + sectionLabels: Record; +} = { + fields: [ + { + id: 'url', + icon: 'link', + label: msg`URL`, + type: msg`Links`, + section: 'General', + visible: true, + }, + { + id: 'account-owner', + icon: 'user', + label: msg`Account Owner`, + type: msg`Relation`, + section: 'General', + visible: true, + }, + { + id: 'revenue', + icon: 'money', + label: msg`Revenue`, + type: msg`Currency`, + section: 'General', + visible: true, + }, + { + id: 'icp', + icon: 'target', + label: msg`ICP`, + type: msg`True/False`, + section: 'Additional', + visible: false, + }, + { + id: 'employees', + icon: 'users', + label: msg`Employees`, + type: msg`Number`, + section: 'Other', + visible: true, + }, + { + id: 'address', + icon: 'map', + label: msg`Address`, + type: msg`Address`, + section: 'Other', + visible: true, + }, + { + id: 'creation-date', + icon: 'calendar', + label: msg`Creation date`, + type: msg`Date and Time`, + section: 'Other', + visible: true, + }, + ], + navItems: [ + { icon: 'building', label: msg`Companies`, isActive: true, color: 'blue' }, + { icon: 'user', label: msg`People`, isActive: false, color: 'blue' }, + { + icon: 'target', + label: msg`Opportunities`, + isActive: false, + color: 'red', + }, + { + icon: 'checkbox', + label: msg`Tasks`, + isActive: false, + color: 'turquoise', + }, + { icon: 'notes', label: msg`Notes`, isActive: false, color: 'turquoise' }, + { + icon: 'dashboard', + label: msg`Dashboards`, + isActive: false, + color: 'gray', + }, + { + icon: 'automation', + label: msg`Workflows`, + isActive: false, + color: 'orange', + isFolder: true, + children: [ + { icon: 'automation', label: msg`Workflows`, color: 'gray' }, + { icon: 'history', label: msg`Workflow Runs`, color: 'gray' }, + { icon: 'versions', label: msg`Workflow Versions`, color: 'gray' }, + ], + }, + ], + sectionLabels: { + Additional: msg`Additional`, + General: msg`General`, + Other: msg`Other`, + }, +}; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/index.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/index.ts new file mode 100644 index 0000000000..4a6686ab46 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/index.ts @@ -0,0 +1 @@ +export { LayoutVisual } from './LayoutVisual'; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-field-definition.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-field-definition.ts new file mode 100644 index 0000000000..7b6014b417 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-field-definition.ts @@ -0,0 +1,12 @@ +import { type MessageDescriptor } from '@lingui/core'; + +import { type LayoutFieldIconType } from './layout-field-icon-type'; + +export type LayoutFieldDefinition = { + icon: LayoutFieldIconType; + id: string; + label: MessageDescriptor; + section: string; + type: MessageDescriptor; + visible: boolean; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-field-icon-type.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-field-icon-type.ts new file mode 100644 index 0000000000..58540b4c90 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-field-icon-type.ts @@ -0,0 +1,8 @@ +export type LayoutFieldIconType = + | 'calendar' + | 'link' + | 'map' + | 'money' + | 'target' + | 'user' + | 'users'; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-nav-icon-type.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-nav-icon-type.ts new file mode 100644 index 0000000000..f317372fcb --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-nav-icon-type.ts @@ -0,0 +1,10 @@ +export type LayoutNavIconType = + | 'automation' + | 'building' + | 'checkbox' + | 'dashboard' + | 'history' + | 'notes' + | 'target' + | 'user' + | 'versions'; diff --git a/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-nav-item-definition.ts b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-nav-item-definition.ts new file mode 100644 index 0000000000..a3d8ca1f0c --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/LayoutVisual/types/layout-nav-item-definition.ts @@ -0,0 +1,20 @@ +import { type MessageDescriptor } from '@lingui/core'; + +import { type PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; + +import { type LayoutNavIconType } from './layout-nav-icon-type'; + +type NavColor = keyof typeof PRODUCT_STEPPER_SCENE.navTiles; + +export type LayoutNavItemDefinition = { + children?: { + color: NavColor; + icon: LayoutNavIconType; + label: MessageDescriptor; + }[]; + color: NavColor; + icon: LayoutNavIconType; + isActive: boolean; + isFolder?: boolean; + label: MessageDescriptor; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/WorkflowVisual.tsx b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/WorkflowVisual.tsx new file mode 100644 index 0000000000..6f3b6537b0 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/WorkflowVisual.tsx @@ -0,0 +1,208 @@ +'use client'; + +import { styled } from '@linaria/react'; +import { useLingui } from '@lingui/react'; + +import { usePointerDragPositions } from '@/platform/motion'; +import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; + +import { STEPPER_SHELL_CHROME } from '../components/ProductStepperShell'; +import { WORKFLOW_GLYPHS } from './components/WorkflowIcons'; +import { WORKFLOW_GRAPH } from './data/workflow-data'; +import { useWorkflowAnimation } from './utils/use-workflow-animation'; + +const shell = PRODUCT_STEPPER_SCENE.shell; +const workflow = PRODUCT_STEPPER_SCENE.workflow; + +const { Canvas, Shell, StageFit, SvgLayer } = STEPPER_SHELL_CHROME; + +const NODE_WIDTH = 170; +const NODE_HEIGHT = 48; + +const ARROW_GAP = 5; + +const ARROW_MARKER_PATH = + 'M1.7915 1.38672H8.18311C8.57541 1.38705 8.81458 1.81852 8.60693 2.15137L5.41064 7.26465C5.21481 7.57798 4.75882 7.57798 4.56299 7.26465L1.3667 2.15137C1.15906 1.81841 1.39896 1.38672 1.7915 1.38672Z'; + +const NodeCard = styled.div<{ $accent: string; $active: boolean }>` + align-items: center; + background: ${shell.cardBackground}; + border: 1px solid + ${({ $accent, $active }) => ($active ? $accent : shell.borderStrong)}; + border-radius: 8px; + box-shadow: ${PRODUCT_STEPPER_SCENE.nodeShadow}; + display: flex; + gap: 8px; + padding: 8px; + position: absolute; + width: ${NODE_WIDTH}px; + z-index: 2; +`; + +const NodeIconBox = styled.div<{ $ink: string }>` + align-items: center; + background: ${shell.tint}; + border-radius: 4px; + color: ${({ $ink }) => $ink}; + display: flex; + flex-shrink: 0; + height: 30px; + justify-content: center; + width: 30px; +`; + +const NodeRight = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: 2px; + min-width: 0; +`; + +const NodeLabel = styled.span` + color: ${shell.textTertiary}; + font-size: 10px; + font-weight: 500; +`; + +const NodeName = styled.div` + color: ${shell.text}; + font-size: 12px; + font-weight: 500; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +function getEdgePath( + from: { x: number; y: number }, + to: { x: number; y: number }, +): string { + const sourceX = from.x + NODE_WIDTH / 2; + const sourceY = from.y + NODE_HEIGHT; + const targetX = to.x + NODE_WIDTH / 2; + const targetY = to.y - ARROW_GAP; + const midY = (sourceY + targetY) / 2; + return `M${sourceX},${sourceY} C${sourceX},${midY} ${targetX},${midY} ${targetX},${targetY}`; +} + +export function WorkflowVisual({ active }: { active: boolean }) { + const { + canvasHandlers, + draggingId: dragging, + handlePointerDown, + positions, + } = usePointerDragPositions(() => + Object.fromEntries( + WORKFLOW_GRAPH.nodes.map((node) => [node.id, { x: node.x, y: node.y }]), + ), + ); + + const activeNodes = useWorkflowAnimation(active); + const { i18n } = useLingui(); + + const sourceNodeIds = [ + ...new Set(WORKFLOW_GRAPH.edges.map((edge) => edge.from)), + ]; + + return ( + + + + + + + + + + + {WORKFLOW_GRAPH.edges.map((edge) => { + const from = positions[edge.from]; + const to = positions[edge.to]; + + if (!from || !to) { + return null; + } + const isActive = + activeNodes.has(edge.from) && activeNodes.has(edge.to); + + return ( + + ); + })} + + + {WORKFLOW_GRAPH.nodes.map((node) => { + const position = positions[node.id]; + const Icon = WORKFLOW_GLYPHS.nodes[node.icon]; + const accent = workflow.accents[node.accent]; + + return ( + handlePointerDown(node.id, event)} + style={{ + cursor: dragging === node.id ? 'grabbing' : 'grab', + left: position.x, + top: position.y, + transition: + dragging === node.id ? 'none' : 'border-color 0.3s ease', + }} + > + + + + + {i18n._(node.type)} + {i18n._(node.label)} + + + ); + })} + + + {sourceNodeIds.map((id) => { + const position = positions[id]; + + if (!position) { + return null; + } + return ( + + ); + })} + + + + + ); +} diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/components/WorkflowIcons.tsx b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/components/WorkflowIcons.tsx new file mode 100644 index 0000000000..baa6f09274 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/components/WorkflowIcons.tsx @@ -0,0 +1,31 @@ +import { + IconBrain, + IconCheck, + IconFilter, + IconPlaylistAdd, + IconPlus, + IconReload, + IconSearch, + IconSend, +} from '@tabler/icons-react'; + +import { type WorkflowIconName } from '../types/workflow-icon-name'; + +const NODE_ICONS: Record = { + brain: IconBrain, + filter: IconFilter, + playlistAdd: IconPlaylistAdd, + plus: IconPlus, + reload: IconReload, + search: IconSearch, + send: IconSend, +}; + +function WorkflowCheckGlyph({ color }: { color: string }) { + return ; +} + +export const WORKFLOW_GLYPHS = { + Check: WorkflowCheckGlyph, + nodes: NODE_ICONS, +}; diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/data/workflow-data.ts b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/data/workflow-data.ts new file mode 100644 index 0000000000..a9554e9409 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/data/workflow-data.ts @@ -0,0 +1,99 @@ +import { msg } from '@lingui/core/macro'; + +import { type WorkflowEdgeDefinition } from '../types/workflow-edge-definition'; +import { type WorkflowNodeDefinition } from '../types/workflow-node-definition'; + +const TRUNK_X = 185; +const LEFT_X = 10; +const RIGHT_X = 360; + +export const WORKFLOW_GRAPH: { + animationSequence: string[]; + edges: WorkflowEdgeDefinition[]; + nodes: WorkflowNodeDefinition[]; + stepIntervalMs: number; +} = { + nodes: [ + { + id: 'trigger', + type: msg`Trigger`, + label: msg`Record is Created`, + icon: 'playlistAdd', + accent: 'blue', + x: TRUNK_X, + y: 16, + }, + { + id: 'filter', + type: msg`Action`, + label: msg`Filter`, + icon: 'filter', + accent: 'green', + x: TRUNK_X, + y: 136, + }, + { + id: 'search', + type: msg`Action`, + label: msg`Search Records`, + icon: 'search', + accent: 'gray', + x: TRUNK_X, + y: 256, + }, + { + id: 'ai', + type: msg`Action`, + label: msg`AI Agent`, + icon: 'brain', + accent: 'pink', + x: TRUNK_X, + y: 376, + }, + { + id: 'update', + type: msg`Action`, + label: msg`Update Record`, + icon: 'reload', + accent: 'gray', + x: LEFT_X, + y: 496, + }, + { + id: 'email', + type: msg`Action`, + label: msg`Send Email`, + icon: 'send', + accent: 'red', + x: TRUNK_X, + y: 496, + }, + { + id: 'create', + type: msg`Action`, + label: msg`Create Record`, + icon: 'plus', + accent: 'gray', + x: RIGHT_X, + y: 496, + }, + ], + edges: [ + { from: 'trigger', to: 'filter' }, + { from: 'filter', to: 'search' }, + { from: 'search', to: 'ai' }, + { from: 'ai', to: 'update' }, + { from: 'ai', to: 'email' }, + { from: 'ai', to: 'create' }, + ], + animationSequence: [ + 'trigger', + 'filter', + 'search', + 'ai', + 'update', + 'email', + 'create', + ], + stepIntervalMs: 800, +}; diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/index.ts b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/index.ts new file mode 100644 index 0000000000..cfb6a24bf0 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/index.ts @@ -0,0 +1 @@ +export { WorkflowVisual } from './WorkflowVisual'; diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-edge-definition.ts b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-edge-definition.ts new file mode 100644 index 0000000000..8e7576ee85 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-edge-definition.ts @@ -0,0 +1,4 @@ +export type WorkflowEdgeDefinition = { + from: string; + to: string; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-icon-name.ts b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-icon-name.ts new file mode 100644 index 0000000000..cd1c714758 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-icon-name.ts @@ -0,0 +1,8 @@ +export type WorkflowIconName = + | 'brain' + | 'filter' + | 'playlistAdd' + | 'plus' + | 'reload' + | 'search' + | 'send'; diff --git a/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-node-definition.ts b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-node-definition.ts new file mode 100644 index 0000000000..fd5fee149a --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/types/workflow-node-definition.ts @@ -0,0 +1,13 @@ +import { type MessageDescriptor } from '@lingui/core'; + +import { type WorkflowIconName } from './workflow-icon-name'; + +export type WorkflowNodeDefinition = { + accent: 'blue' | 'gray' | 'green' | 'pink' | 'red'; + icon: WorkflowIconName; + id: string; + label: MessageDescriptor; + type: MessageDescriptor; + x: number; + y: number; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/utils/use-workflow-animation.ts b/packages/twenty-website/src/sections/product-stepper/WorkflowVisual/utils/use-workflow-animation.ts similarity index 100% rename from packages/twenty-website/src/sections/product-stepper/utils/use-workflow-animation.ts rename to packages/twenty-website/src/sections/product-stepper/WorkflowVisual/utils/use-workflow-animation.ts diff --git a/packages/twenty-website/src/sections/product-stepper/components/DataModelVisual.tsx b/packages/twenty-website/src/sections/product-stepper/components/DataModelVisual.tsx deleted file mode 100644 index fa0b360eff..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/components/DataModelVisual.tsx +++ /dev/null @@ -1,283 +0,0 @@ -'use client'; - -import { styled } from '@linaria/react'; -import { useState } from 'react'; - -import { usePointerDragPositions } from '@/platform/motion'; -import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -import { - DATA_MODEL_GRAPH, - type EntityConnection, -} from '../data/data-model-data'; -import { DATA_MODEL_ICONS } from './DataModelIcons'; -import { DrawEdge } from './DrawEdge'; -import { STEPPER_SHELL_CHROME } from './ProductStepperShell'; - -const shell = PRODUCT_STEPPER_SCENE.shell; -const badges = PRODUCT_STEPPER_SCENE.badges; -const entityTones = PRODUCT_STEPPER_SCENE.entityTones; - -const { Canvas, Shell, SvgLayer } = STEPPER_SHELL_CHROME; - -const EntityCard = styled.div` - backdrop-filter: blur(14px); - background: ${shell.cardBackground}; - border: 1px solid ${shell.borderMedium}; - border-radius: 8px; - box-shadow: ${PRODUCT_STEPPER_SCENE.cardShadow}; - 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; - - &[data-hovered] { - border-color: ${shell.borderStrong}; - } - - &: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 ${shell.borderLight}; - border-radius: 4px; - display: flex; - flex-direction: column; - gap: 2px; - overflow: hidden; - padding: 4px 0; -`; - -const EntityIcon = styled.span<{ $tint: string; $line: string }>` - align-items: center; - background: ${({ $tint }) => $tint}; - border: 1px solid ${({ $line }) => $line}; - border-radius: 3px; - color: ${shell.text}; - display: flex; - height: 14px; - justify-content: center; - width: 14px; -`; - -const EntityLabel = styled.span` - color: ${shell.text}; - font-size: 12px; - font-weight: 500; - line-height: 1.4; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -`; - -const EntityMeta = styled.span` - color: ${shell.textTertiary}; - 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<{ - $ink: string; - $line: string; - $tint: string; -}>` - align-items: center; - background: ${({ $tint }) => $tint}; - border: 1px solid ${({ $line }) => $line}; - border-radius: 2px; - color: ${({ $ink }) => $ink}; - display: flex; - font-size: 8px; - font-weight: 500; - height: 14px; - justify-content: center; - width: 14px; -`; - -const MetaBadgeText = styled.span` - color: ${shell.textMuted}; - font-size: 9px; - font-weight: 400; -`; - -const FieldRow = styled.div` - align-items: center; - color: ${shell.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: ${shell.textMuted}; - display: flex; - height: 14px; - justify-content: center; - width: 14px; -`; - -const ExpandHint = styled.div` - align-items: center; - color: ${shell.textTertiary}; - 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; - -type EntityPositions = Record; - -function getCardCenter( - positions: EntityPositions, - entityId: string, -): { x: number; y: number } { - const position = positions[entityId]; - - if (!position) { - return { x: 0, y: 0 }; - } - return { - x: position.x + CARD_WIDTH / 2, - y: position.y + CARD_HEIGHT_ESTIMATE / 2, - }; -} - -export function DataModelVisual({ active }: { active: boolean }) { - const { - canvasHandlers, - draggingId: dragging, - handlePointerDown, - positions, - } = usePointerDragPositions(() => - Object.fromEntries( - DATA_MODEL_GRAPH.entities.map((entity) => [ - entity.id, - { x: entity.x, y: entity.y }, - ]), - ), - ); - const [hoveredEntity, setHoveredEntity] = useState(null); - - const isConnectionHighlighted = (connection: EntityConnection) => - hoveredEntity === connection.from || hoveredEntity === connection.to; - - return ( - - - - {DATA_MODEL_GRAPH.connections.map((connection) => ( - - ))} - - - {DATA_MODEL_GRAPH.entities.map((entity) => { - const position = positions[entity.id]; - const HeaderIcon = DATA_MODEL_ICONS.headers[entity.headerIcon]; - const tone = entityTones[entity.tone]; - const badge = entity.isCustom ? badges.custom : badges.standard; - - return ( - handlePointerDown(entity.id, event)} - onPointerEnter={() => setHoveredEntity(entity.id)} - onPointerLeave={() => setHoveredEntity(null)} - style={{ - left: position.x, - top: position.y, - transition: - dragging === entity.id ? 'none' : 'border-color 0.15s', - }} - > - - - - - {entity.label} - · {entity.meta} - - - L - - - {entity.isCustom ? 'Custom' : 'Standard'} - - - - - {entity.fields.map((field) => { - const Icon = DATA_MODEL_ICONS.fields[field.icon]; - return ( - - - - - {field.label} - - ); - })} - - {entity.expandCount} fields - - - - ); - })} - - - ); -} diff --git a/packages/twenty-website/src/sections/product-stepper/components/LayoutIcons.tsx b/packages/twenty-website/src/sections/product-stepper/components/LayoutIcons.tsx deleted file mode 100644 index a64f53d236..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/components/LayoutIcons.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -const inks = PRODUCT_STEPPER_SCENE.layout; - -export type LayoutFieldIconType = - | 'calendar' - | 'link' - | 'map' - | 'money' - | 'target' - | 'user' - | 'users'; - -export type LayoutNavIconType = - | 'ai' - | 'automation' - | 'building' - | 'checkbox' - | 'letterS' - | 'notes' - | 'play' - | 'stripeS' - | 'target' - | 'user' - | 'versions'; - -function FieldGlyph({ type }: { type: LayoutFieldIconType }) { - const ink = inks.fieldInk; - const size = 10; - switch (type) { - case 'link': - return ( - - - - ); - case 'user': - return ( - - - - - ); - case 'money': - return ( - - - - ); - case 'target': - return ( - - - - - - ); - case 'users': - return ( - - - - - - - ); - case 'map': - return ( - - - - - ); - case 'calendar': - return ( - - - - - ); - } -} - -function NavGlyph({ type }: { type: LayoutNavIconType }) { - const ink = inks.navInk; - const size = 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 'letterS': - return ( - - - S - - - ); - case 'stripeS': - return ( - - - S - - - ); - } -} - -function EyeGlyph({ visible }: { visible: boolean }) { - if (visible) { - return ( - - - - - ); - } - return ( - - - - - ); -} - -function ChevronLeftGlyph() { - return ( - - - - ); -} - -function DotsGlyph() { - return ( - - - - - - ); -} - -function DotsWhiteGlyph() { - return ( - - - - - - ); -} - -function GripGlyph() { - return ( - - - - - - - - - ); -} - -function PaintGlyph() { - return ( - - - - - ); -} - -function CheckGlyph() { - return ( - - - - ); -} - -function ListGlyph() { - return ( - - - - ); -} - -function SparkGlyph() { - return ( - - - - ); -} - -function NewSectionGlyph() { - return ( - - - - ); -} - -function PlusGlyph() { - return ( - - - - ); -} - -export const LAYOUT_GLYPHS = { - Check: CheckGlyph, - ChevronLeft: ChevronLeftGlyph, - Dots: DotsGlyph, - DotsWhite: DotsWhiteGlyph, - Eye: EyeGlyph, - Field: FieldGlyph, - Grip: GripGlyph, - List: ListGlyph, - Nav: NavGlyph, - NewSection: NewSectionGlyph, - Paint: PaintGlyph, - Plus: PlusGlyph, - Spark: SparkGlyph, -}; diff --git a/packages/twenty-website/src/sections/product-stepper/components/LayoutVisual.tsx b/packages/twenty-website/src/sections/product-stepper/components/LayoutVisual.tsx deleted file mode 100644 index c480081c50..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/components/LayoutVisual.tsx +++ /dev/null @@ -1,347 +0,0 @@ -'use client'; - -import { - useRef, - useState, - type PointerEvent as ReactPointerEvent, -} from 'react'; - -import { LAYOUT_CHROME } from '../data/layout-chrome'; -import { LAYOUT_EDITOR_CONTENT } from '../data/layout-data'; -import { LAYOUT_GLYPHS } from './LayoutIcons'; -import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -const { - Check: CheckGlyph, - ChevronLeft: ChevronLeftGlyph, - Dots: DotsGlyph, - DotsWhite: DotsWhiteGlyph, - Eye: EyeGlyph, - Field: FieldGlyph, - Grip: GripGlyph, - List: ListGlyph, - Nav: NavGlyph, - NewSection: NewSectionGlyph, - Paint: PaintGlyph, - Plus: PlusGlyph, - Spark: SparkGlyph, -} = LAYOUT_GLYPHS; - -const { - ActionButton, - ActionsBar, - AddIconBox, - AddSectionRow, - AddText, - BlueHeader, - Canvas, - DoneButton, - EditableRow, - EditableText, - FieldDot, - FieldIconBox, - FieldLabels, - FieldName, - FieldRow, - FieldType, - HeaderCenter, - HeaderLeft, - HeaderSave, - HeaderTitle, - MainCard, - NavBreadcrumb, - NavChevron, - NavIconBox, - NavItem, - NavPanel, - NavSectionLabel, - NavSubItem, - NavSuffix, - NewFieldsColumn, - NewFieldsDescription, - NewFieldsRow, - NewFieldsTitle, - PanelActionButton, - PanelBackButton, - PanelFields, - PanelHeader, - PanelIconBox, - PanelSubBar, - PanelSubLabel, - PanelTitleBold, - PanelTitleGroup, - PanelTitleSub, - RightPanel, - SectionName, - SectionRow, - WidgetChip, - WidgetIcon, - WidgetInner, - WidgetLabel, - WidgetPanel, - WidgetRow, - WidgetSectionLabel, - WidgetTitle, - WidgetValue, -} = LAYOUT_CHROME; - -const ROW_STEP_PX = 22; - -export function LayoutVisual({ active }: { active: boolean }) { - const [fields, setFields] = useState(LAYOUT_EDITOR_CONTENT.fields); - const [draggingId, setDraggingId] = useState(null); - const dragStartY = useRef(0); - - const toggleVisibility = (fieldId: string) => { - setFields((previous) => - previous.map((field) => - field.id === fieldId ? { ...field, visible: !field.visible } : field, - ), - ); - }; - - const handleDragStart = ( - fieldId: string, - event: ReactPointerEvent, - ) => { - event.preventDefault(); - event.currentTarget.setPointerCapture(event.pointerId); - setDraggingId(fieldId); - dragStartY.current = event.clientY; - }; - - const handleDragMove = (event: ReactPointerEvent) => { - if (!draggingId) { - return; - } - const deltaY = event.clientY - dragStartY.current; - const steps = Math.round(deltaY / ROW_STEP_PX); - - if (steps === 0) { - return; - } - setFields((previous) => { - const index = previous.findIndex((field) => field.id === draggingId); - - if (index === -1) { - return previous; - } - const target = Math.max(0, Math.min(previous.length - 1, index + steps)); - - if (target === index) { - return previous; - } - const next = [...previous]; - const [moved] = next.splice(index, 1); - next.splice(target, 0, moved); - return next; - }); - dragStartY.current = event.clientY; - }; - - const handleDragEnd = () => setDraggingId(null); - const handleLostCapture = () => { - if (draggingId) { - setDraggingId(null); - } - }; - const sections = [...new Set(fields.map((field) => field.section))]; - - return ( - - - - - - - - - Layout edition - - - Save - - - - - - - Widget name - General - - - - - URL - qonto.com - - - - - - Account O... - Phil Schiller - - - - - - Address - 18 Rue De Navarin, 750... - - - - - - ICP - ✓ True - - - - - - Workspace - {LAYOUT_EDITOR_CONTENT.navItems.map((item) => ( -
- - - - - {item.label} - {item.suffix ? · {item.suffix} : null} - {item.isFolder ? : null} - - {item.children?.map((child) => ( - - - - - - {child.label} - - ))} -
- ))} -
- - - + New record - ✧ Enrich - ✎ Edit actions - - - - - - - - - - - - Fields - Fields widget - - - - - - - - - - - Layout - - - - {sections.map((section, sectionNumber) => ( -
- - - {section} - - - - - - {sectionNumber === 0 ? ( - - Industry - Done - - ) : null} - - {fields - .filter((field) => field.section === section) - .map((field) => ( - handleDragStart(field.id, event)} - > - - - - - {field.label} - · - {field.type} - - { - event.stopPropagation(); - toggleVisibility(field.id); - }} - > - - - - - - - ))} - - {sectionNumber > 0 && sectionNumber < sections.length - 1 ? ( - - - - - Add a Section - - ) : null} -
- ))} - - - - - - - New fields - - Default position/visibility for field… - - - - - - - - - Add a Section - -
-
-
- ); -} diff --git a/packages/twenty-website/src/sections/product-stepper/components/ProductStepperContent.tsx b/packages/twenty-website/src/sections/product-stepper/components/ProductStepperContent.tsx index faaae0c983..7baf4ed06b 100644 --- a/packages/twenty-website/src/sections/product-stepper/components/ProductStepperContent.tsx +++ b/packages/twenty-website/src/sections/product-stepper/components/ProductStepperContent.tsx @@ -16,10 +16,8 @@ import { StepperSwipeDeck, } from '@/ui'; -import { - PRODUCT_STEPPER_STEPS, - type ProductStepperStep, -} from '../data/product-stepper-data'; +import { PRODUCT_STEPPER_STEPS } from '../data/product-stepper-data'; +import { type ProductStepperStep } from '../types/product-stepper-step'; const ContentRoot = styled.div` display: grid; diff --git a/packages/twenty-website/src/sections/product-stepper/components/ProductStepperShell.tsx b/packages/twenty-website/src/sections/product-stepper/components/ProductStepperShell.tsx index fbe1ef1916..07adf6b925 100644 --- a/packages/twenty-website/src/sections/product-stepper/components/ProductStepperShell.tsx +++ b/packages/twenty-website/src/sections/product-stepper/components/ProductStepperShell.tsx @@ -1,101 +1,49 @@ -import { styled } from '@linaria/react'; -import { type ReactNode } from 'react'; +'use client'; +import { styled } from '@linaria/react'; +import { useRef, type ReactNode } from 'react'; + +import { useScaleToFit } from '@/platform/motion'; import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; const shell = PRODUCT_STEPPER_SCENE.shell; const Wrapper = styled.div` - background: ${shell.background}; - border-radius: 2px; - box-shadow: ${shell.shadowSm}; display: flex; flex-direction: column; font-family: ${shell.font}; - height: 92%; - margin-left: auto; - margin-top: auto; + height: 100%; opacity: 0.7; overflow: hidden; transition: opacity 0.3s; - width: 88%; + width: 100%; &[data-active] { opacity: 1; } `; -const Header = styled.div` - align-items: center; - display: flex; - gap: 4px; - height: 30px; - padding: 0 10px; -`; - -const HeaderLogo = styled.span` - align-items: center; - background: ${shell.headerBackground}; - border: 1px solid ${shell.headerBorder}; - border-radius: 3px; - color: ${shell.text}; - display: flex; - height: 14px; - justify-content: center; - width: 14px; -`; - -const HeaderTitle = styled.span` - color: ${shell.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 HeaderButton = styled.span` - align-items: center; - border: 1px solid ${shell.borderMedium}; - border-radius: 4px; - color: ${shell.textMuted}; - display: flex; - height: 22px; - justify-content: center; - width: 22px; -`; - -const HeaderCommandButton = styled.span` - align-items: center; - border: 1px solid ${shell.borderSubtle}; - border-radius: 4px; - color: ${shell.textTertiary}; - display: flex; - font-size: 12px; - font-weight: 500; - gap: 4px; - height: 22px; - padding: 0 6px; -`; - const Canvas = styled.div` - background: white; - border: 1px solid ${shell.borderMedium}; - border-radius: 8px; flex: 1; - margin: 0 10px 10px; min-height: 0; overflow: hidden; position: relative; user-select: none; `; +const StageFitContainer = styled.div` + align-items: center; + display: flex; + height: 100%; + justify-content: center; + width: 100%; +`; + +const StageBox = styled.div` + flex-shrink: 0; + position: relative; +`; + const SvgLayer = styled.svg` height: 100%; left: 0; @@ -105,92 +53,45 @@ const SvgLayer = styled.svg` width: 100%; `; -function Shell({ - active, +function Shell({ active, children }: { active: boolean; children: ReactNode }) { + return {children}; +} + +function StageFit({ + baseScale = 1, children, - title, + designHeight, + designWidth, + zoom = 1, }: { - active: boolean; + baseScale?: number; children: ReactNode; - title: string; + designHeight: number; + designWidth: number; + zoom?: number; }) { + const containerRef = useRef(null); + const fit = useScaleToFit(containerRef, designWidth, designHeight, 1); + const scale = Math.min(baseScale, zoom * fit || baseScale); + return ( - -
- - - - - - - - - - {title} - - - - - - - - - - - - - - - - - - ⌘K - - -
- {children} -
+ + + {children} + + ); } export const STEPPER_SHELL_CHROME = { Canvas, Shell, + StageFit, SvgLayer, }; diff --git a/packages/twenty-website/src/sections/product-stepper/components/ProductStepperVisual.tsx b/packages/twenty-website/src/sections/product-stepper/components/ProductStepperVisual.tsx index 9289180e5b..da4dfd1465 100644 --- a/packages/twenty-website/src/sections/product-stepper/components/ProductStepperVisual.tsx +++ b/packages/twenty-website/src/sections/product-stepper/components/ProductStepperVisual.tsx @@ -3,16 +3,14 @@ import { styled } from '@linaria/react'; import { type ComponentType } from 'react'; -import { mediaUp } from '@/tokens'; +import { EASING, mediaUp } from '@/tokens'; -import { DataModelVisual } from './DataModelVisual'; -import { LayoutVisual } from './LayoutVisual'; -import { - PRODUCT_STEPPER_STEPS, - type ProductStepperVisualKey, -} from '../data/product-stepper-data'; +import { DataModelVisual } from '../DataModelVisual'; +import { LayoutVisual } from '../LayoutVisual'; +import { WorkflowVisual } from '../WorkflowVisual'; +import { PRODUCT_STEPPER_STEPS } from '../data/product-stepper-data'; +import { type ProductStepperVisualKey } from '../types/product-stepper-visual-key'; import { ProductStepperVisualFrame } from './ProductStepperVisualFrame'; -import { WorkflowVisual } from './WorkflowVisual'; const VISUALS: Record< ProductStepperVisualKey, @@ -54,11 +52,15 @@ const Slide = styled.div` opacity: 0; pointer-events: none; position: absolute; - transition: opacity 0.4s ease; + transform: translateY(18px) scale(0.985); + transition: + opacity 0.55s ${EASING.gentle}, + transform 0.55s ${EASING.standard}; &[data-active] { opacity: 1; pointer-events: auto; + transform: translateY(0) scale(1); } `; diff --git a/packages/twenty-website/src/sections/product-stepper/components/WorkflowIcons.tsx b/packages/twenty-website/src/sections/product-stepper/components/WorkflowIcons.tsx deleted file mode 100644 index 5a2fc34e15..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/components/WorkflowIcons.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -export type WorkflowIconName = - | 'playlistAdd' - | 'plus' - | 'reload' - | 'repeat' - | 'search' - | 'send'; - -function IconPlaylistAdd() { - return ( - - - - - - - - ); -} - -function IconSearch() { - return ( - - - - - ); -} - -function IconRepeat() { - return ( - - - - - ); -} - -function IconSend() { - return ( - - - - - ); -} - -function IconReload() { - return ( - - - - - ); -} - -function IconPlus() { - return ( - - - - - ); -} - -function WorkflowCheckGlyph({ color }: { color: string }) { - return ( - - - - ); -} - -export const WORKFLOW_GLYPHS = { - Check: WorkflowCheckGlyph, - nodes: { - playlistAdd: IconPlaylistAdd, - plus: IconPlus, - reload: IconReload, - repeat: IconRepeat, - search: IconSearch, - send: IconSend, - }, -}; diff --git a/packages/twenty-website/src/sections/product-stepper/components/WorkflowVisual.tsx b/packages/twenty-website/src/sections/product-stepper/components/WorkflowVisual.tsx deleted file mode 100644 index 67ccc306a4..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/components/WorkflowVisual.tsx +++ /dev/null @@ -1,273 +0,0 @@ -'use client'; - -import { styled } from '@linaria/react'; - -import { usePointerDragPositions } from '@/platform/motion'; -import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -import { DrawEdge } from './DrawEdge'; -import { STEPPER_SHELL_CHROME } from './ProductStepperShell'; -import { useWorkflowAnimation } from '../utils/use-workflow-animation'; -import { WORKFLOW_GRAPH } from '../data/workflow-data'; -import { WORKFLOW_GLYPHS } from './WorkflowIcons'; - -const shell = PRODUCT_STEPPER_SCENE.shell; -const workflow = PRODUCT_STEPPER_SCENE.workflow; - -const { Canvas, Shell, SvgLayer } = STEPPER_SHELL_CHROME; - -const NODE_WIDTH = WORKFLOW_GRAPH.nodeWidthPx; -const NODE_HEIGHT = WORKFLOW_GRAPH.nodeHeightPx; - -const LABEL_TONES = { - amber: workflow.amber, - gray: workflow.gray, - green: workflow.green, -}; - -const NodeCard = styled.div` - align-items: center; - background: ${shell.cardBackground}; - border: 1px solid ${shell.borderStrong}; - border-radius: 8px; - box-shadow: ${PRODUCT_STEPPER_SCENE.nodeShadow}; - display: flex; - gap: 8px; - min-width: ${NODE_WIDTH}px; - padding: 8px; - position: absolute; - z-index: 2; -`; - -const NodeIconBox = styled.div` - align-items: center; - background: ${shell.tint}; - border-radius: 4px; - color: ${shell.textMuted}; - 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<{ $ink: string }>` - color: ${({ $ink }) => $ink}; - font-size: 10px; - font-weight: 600; -`; - -const NodeCheck = styled.span<{ $tint: string }>` - align-items: center; - background: ${({ $tint }) => $tint}; - border-radius: 2px; - display: flex; - height: 12px; - justify-content: center; - opacity: 0; - transition: opacity 0.3s; - width: 12px; - - &[data-visible] { - opacity: 1; - } -`; - -const NodeName = styled.div` - color: ${shell.text}; - font-size: 12px; - font-weight: 500; - line-height: 1.4; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &[data-dimmed] { - color: ${shell.textTertiary}; - } -`; - -const IterationLabel = styled.span` - background: ${shell.cardBackground}; - border: 1px solid ${shell.borderStrong}; - border-radius: 4px; - color: ${shell.textTertiary}; - font-size: 9px; - font-weight: 600; - padding: 2px 4px; - position: absolute; - white-space: nowrap; - z-index: 3; -`; - -type NodePositions = Record; - -function getNodeCenter(position: { x: number; y: number }): { - x: number; - y: number; -} { - return { x: position.x + NODE_WIDTH / 2, y: position.y + NODE_HEIGHT / 2 }; -} - -function IteratorLoopPath({ positions }: { positions: NodePositions }) { - const iteratorPosition = positions.iterator; - const emailPosition = positions.email; - - if (!iteratorPosition || !emailPosition) { - return null; - } - - const startX = iteratorPosition.x + NODE_WIDTH; - const startY = iteratorPosition.y + NODE_HEIGHT / 2; - const horizontalEnd = startX + 117; - const cornerRadius = 8; - const verticalEnd = emailPosition.y; - const emailCenterX = emailPosition.x + NODE_WIDTH / 2; - const midY = verticalEnd - 28; - const loopColor = shell.borderStrong; - const arrowHalf = 3; - - return ( - - - - - - - ); -} - -export function WorkflowVisual({ active }: { active: boolean }) { - const { - canvasHandlers, - draggingId: dragging, - handlePointerDown, - positions, - } = usePointerDragPositions(() => - Object.fromEntries( - WORKFLOW_GRAPH.nodes.map((node) => [node.id, { x: node.x, y: node.y }]), - ), - ); - - const activeNodes = useWorkflowAnimation(active); - - return ( - - - - {WORKFLOW_GRAPH.edges.map((edge) => { - const fromPosition = positions[edge.from]; - const toPosition = positions[edge.to]; - - if (!fromPosition || !toPosition) { - return null; - } - return ( - - ); - })} - - - - {WORKFLOW_GRAPH.nodes.map((node) => { - const position = positions[node.id]; - const Icon = WORKFLOW_GLYPHS.nodes[node.icon]; - const checkTint = node.dimmed - ? workflow.grayBackground - : workflow.tealBackground; - const checkInk = node.dimmed ? workflow.gray : workflow.green; - - return ( - handlePointerDown(node.id, event)} - style={{ - cursor: dragging === node.id ? 'grabbing' : 'grab', - left: position.x, - top: position.y, - transition: dragging === node.id ? 'none' : 'box-shadow 0.15s', - }} - > - - - - - - - {node.type} - - {node.badge ? ( - <> - - {node.badge} - - - - - - ) : null} - - - {node.label} - - - - ); - })} - - - iteration 2/3 - - - - ); -} diff --git a/packages/twenty-website/src/sections/product-stepper/data/data-model-data.ts b/packages/twenty-website/src/sections/product-stepper/data/data-model-data.ts deleted file mode 100644 index 7248b5633a..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/data/data-model-data.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { type PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -import { - type DataModelFieldIcon, - type DataModelHeaderIcon, -} from '../components/DataModelIcons'; - -export type EntityTone = keyof typeof PRODUCT_STEPPER_SCENE.entityTones; - -export type EntityDefinition = { - expandCount: number; - fields: { icon: DataModelFieldIcon; label: string }[]; - headerIcon: DataModelHeaderIcon; - id: string; - isCustom: boolean; - label: string; - meta: string; - tone: EntityTone; - x: number; - y: number; -}; - -export type EntityConnection = { - from: string; - to: string; -}; - -export const DATA_MODEL_GRAPH: { - connections: EntityConnection[]; - entities: EntityDefinition[]; -} = { - entities: [ - { - id: 'workspaces', - label: 'Workspaces', - meta: '22', - isCustom: true, - headerIcon: 'userScreenSmall', - tone: 'green', - fields: [ - { icon: 'building', label: 'Company' }, - { icon: 'user', label: 'Users' }, - ], - expandCount: 21, - x: 40, - y: 40, - }, - { - id: 'companies', - label: 'Companies', - meta: '39', - isCustom: false, - headerIcon: 'buildingSmall', - tone: 'indigo', - fields: [ - { icon: 'apps', label: 'Workspace' }, - { icon: 'tag', label: '31 fields' }, - ], - expandCount: 8, - x: 290, - y: 20, - }, - { - id: 'users', - label: 'Users', - meta: '497', - isCustom: true, - headerIcon: 'usersSmall', - tone: 'purple', - fields: [ - { icon: 'user', label: 'People' }, - { icon: 'apps', label: 'Workspace' }, - ], - expandCount: 32, - x: 40, - y: 310, - }, - { - id: 'people', - label: 'People', - meta: '648', - isCustom: false, - headerIcon: 'userSmall', - tone: 'indigo', - fields: [ - { icon: 'building', label: 'Company' }, - { icon: 'user', label: 'Users' }, - { icon: 'target', label: 'Opportunity' }, - ], - expandCount: 4, - x: 280, - y: 400, - }, - { - id: 'opportunities', - label: 'Opportunities', - meta: '42', - isCustom: false, - headerIcon: 'targetSmall', - tone: 'red', - fields: [ - { icon: 'building', label: 'Company' }, - { icon: 'tag', label: '12 fields' }, - ], - expandCount: 23, - x: 380, - y: 190, - }, - ], - connections: [ - { 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/src/sections/product-stepper/data/layout-data.ts b/packages/twenty-website/src/sections/product-stepper/data/layout-data.ts deleted file mode 100644 index f9dc65ff16..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/data/layout-data.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { type PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene'; - -import { - type LayoutFieldIconType, - type LayoutNavIconType, -} from '../components/LayoutIcons'; - -type NavTint = keyof typeof PRODUCT_STEPPER_SCENE.navTints; - -export type LayoutFieldDefinition = { - icon: LayoutFieldIconType; - id: string; - label: string; - section: string; - type: string; - visible: boolean; -}; - -export type LayoutNavItemDefinition = { - background: NavTint; - children?: { background: NavTint; icon: LayoutNavIconType; label: string }[]; - icon: LayoutNavIconType; - isActive: boolean; - isFolder?: boolean; - label: string; - suffix?: string; -}; - -export const LAYOUT_EDITOR_CONTENT: { - fields: LayoutFieldDefinition[]; - navItems: LayoutNavItemDefinition[]; -} = { - fields: [ - { - 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, - }, - ], - navItems: [ - { - icon: 'building', - label: 'Companies', - isActive: true, - background: 'indigo', - }, - { icon: 'user', label: 'People', isActive: false, background: 'indigo' }, - { - icon: 'target', - label: 'Opportunities', - isActive: false, - background: 'red', - }, - { - icon: 'checkbox', - label: 'Tasks', - isActive: false, - background: 'teal', - }, - { icon: 'notes', label: 'Notes', isActive: false, background: 'teal' }, - { - icon: 'letterS', - label: 'Sales Dashboard', - isActive: false, - background: 'yellow', - suffix: 'Dashboard', - }, - { - icon: 'automation', - label: 'Workflows', - isActive: false, - background: 'peach', - isFolder: true, - children: [ - { icon: 'automation', label: 'Workflows', background: 'gray' }, - { icon: 'play', label: 'Workflows runs', background: 'gray' }, - { - icon: 'versions', - label: 'Workflows versions', - background: 'gray', - }, - ], - }, - { icon: 'ai', label: 'Claude', isActive: false, background: 'gray' }, - { - icon: 'stripeS', - label: 'Stripe', - isActive: false, - background: 'gray', - isFolder: true, - }, - ], -}; diff --git a/packages/twenty-website/src/sections/product-stepper/data/product-stepper-data.ts b/packages/twenty-website/src/sections/product-stepper/data/product-stepper-data.ts index a8fecbc758..12db980c37 100644 --- a/packages/twenty-website/src/sections/product-stepper/data/product-stepper-data.ts +++ b/packages/twenty-website/src/sections/product-stepper/data/product-stepper-data.ts @@ -1,16 +1,6 @@ -import { type MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; -import { type INFORMATIVE_MARKS } from '@/icons'; - -export type ProductStepperVisualKey = 'dataModel' | 'layout' | 'workflow'; - -export type ProductStepperStep = { - body: MessageDescriptor; - heading: MessageDescriptor; - icon: keyof typeof INFORMATIVE_MARKS; - visual: ProductStepperVisualKey; -}; +import { type ProductStepperStep } from '../types/product-stepper-step'; export const PRODUCT_STEPPER_STEPS: readonly ProductStepperStep[] = [ { diff --git a/packages/twenty-website/src/sections/product-stepper/data/workflow-data.ts b/packages/twenty-website/src/sections/product-stepper/data/workflow-data.ts deleted file mode 100644 index 0c1236a29d..0000000000 --- a/packages/twenty-website/src/sections/product-stepper/data/workflow-data.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { type WorkflowIconName } from '../components/WorkflowIcons'; - -export type WorkflowNodeDefinition = { - badge?: string; - dimmed?: boolean; - icon: WorkflowIconName; - id: string; - label: string; - labelTone: 'amber' | 'gray' | 'green'; - type: string; - x: number; - y: number; -}; - -export type WorkflowEdgeDefinition = { - from: string; - to: string; -}; - -const TRUNK_X = 55; -const RIGHT_X = 200; -const LEFT_X = 5; - -export const WORKFLOW_GRAPH: { - animationSequence: string[]; - edges: WorkflowEdgeDefinition[]; - nodeHeightPx: number; - nodeWidthPx: number; - nodes: WorkflowNodeDefinition[]; - stepIntervalMs: number; -} = { - nodes: [ - { - id: 'trigger', - type: 'Trigger', - label: 'Record is Created', - icon: 'playlistAdd', - labelTone: 'green', - x: TRUNK_X, - y: 16, - badge: '1', - }, - { - id: 'search', - type: 'Action', - label: 'Search Records', - icon: 'search', - labelTone: 'green', - x: TRUNK_X, - y: 92, - badge: '1', - }, - { - id: 'iterator', - type: 'Flow', - label: 'Iterator', - icon: 'repeat', - labelTone: 'amber', - x: TRUNK_X, - y: 168, - }, - { - id: 'email', - type: 'Action', - label: 'Send Email', - icon: 'send', - labelTone: 'amber', - x: RIGHT_X, - y: 280, - }, - { - id: 'update', - type: 'Action', - label: 'Update Record', - icon: 'reload', - labelTone: 'gray', - x: LEFT_X, - y: 340, - badge: '3', - dimmed: true, - }, - { - id: 'create', - type: 'Action', - label: 'Create Record', - icon: 'plus', - labelTone: 'green', - x: RIGHT_X - 5, - y: 400, - badge: '1', - }, - ], - edges: [ - { from: 'trigger', to: 'search' }, - { from: 'search', to: 'iterator' }, - { from: 'iterator', to: 'update' }, - { from: 'email', to: 'create' }, - ], - animationSequence: [ - 'trigger', - 'search', - 'iterator', - 'email', - 'update', - 'create', - ], - stepIntervalMs: 800, - nodeWidthPx: 170, - nodeHeightPx: 48, -}; diff --git a/packages/twenty-website/src/sections/product-stepper/types/product-stepper-step.ts b/packages/twenty-website/src/sections/product-stepper/types/product-stepper-step.ts new file mode 100644 index 0000000000..e6d77819e9 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/types/product-stepper-step.ts @@ -0,0 +1,12 @@ +import { type MessageDescriptor } from '@lingui/core'; + +import { type INFORMATIVE_MARKS } from '@/icons'; + +import { type ProductStepperVisualKey } from './product-stepper-visual-key'; + +export type ProductStepperStep = { + body: MessageDescriptor; + heading: MessageDescriptor; + icon: keyof typeof INFORMATIVE_MARKS; + visual: ProductStepperVisualKey; +}; diff --git a/packages/twenty-website/src/sections/product-stepper/types/product-stepper-visual-key.ts b/packages/twenty-website/src/sections/product-stepper/types/product-stepper-visual-key.ts new file mode 100644 index 0000000000..048ce5d0d3 --- /dev/null +++ b/packages/twenty-website/src/sections/product-stepper/types/product-stepper-visual-key.ts @@ -0,0 +1 @@ +export type ProductStepperVisualKey = 'dataModel' | 'layout' | 'workflow'; diff --git a/packages/twenty-website/src/tokens/feature-scenes/product-stepper-scene.ts b/packages/twenty-website/src/tokens/feature-scenes/product-stepper-scene.ts index e548bab102..895b315430 100644 --- a/packages/twenty-website/src/tokens/feature-scenes/product-stepper-scene.ts +++ b/packages/twenty-website/src/tokens/feature-scenes/product-stepper-scene.ts @@ -37,32 +37,32 @@ export const PRODUCT_STEPPER_SCENE = { }, workflow: { green: '#30a46c', - amber: '#946800', gray: FICTION_PALETTE.inkFaint, tealBackground: '#e7f9f5', grayBackground: '#f9f9f9', + accents: { + blue: '#3e63dd', + gray: FICTION_PALETTE.inkMuted, + green: '#193b2d', + pink: '#d6409f', + red: '#e5484d', + }, }, - navTints: { - gray: FICTION_PALETTE.borderMedium, - indigo: '#d9e2fc', - peach: '#ffdcc3', - red: '#fdd8d8', - teal: '#c7ebe5', - yellow: FICTION_PALETTE.yellowWash, + navTiles: { + blue: { background: '#d2deff', border: '#c1d0ff', icon: '#3a5bc7' }, + red: { background: '#ffcdce', border: '#fdbdbe', icon: '#ce2c31' }, + turquoise: { background: '#b8eae0', border: '#a1ded2', icon: '#008573' }, + gray: { background: '#ebebeb', border: '#d6d6d6', icon: '#666666' }, + orange: { background: '#ffd19a', border: '#ffc182', icon: '#cc4e00' }, }, layout: { accent: FICTION_PALETTE.accent, panelAccent: '#4a38f5', - glass: 'rgba(255, 255, 255, 0.9)', dragWash: 'rgba(59, 130, 246, 0.04)', chipWash: 'rgba(0, 0, 0, 0.02)', - saveBorder: 'rgba(255, 255, 255, 0.7)', fieldInk: FICTION_PALETTE.inkMuted, - navInk: '#555', eyeInk: FICTION_PALETTE.inkFaint, eyeHiddenInk: FICTION_PALETTE.inkDisabled, - letterSInk: '#35290f', - stripeSInk: '#333', }, cardShadow: '0 0 2px rgba(0, 0, 0, 0.08), 0 2px 2px rgba(0, 0, 0, 0.04)', nodeShadow: '0 0 2px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04)',