Files
twenty/packages/twenty-website/src/sections/product-stepper/components/LayoutVisual.tsx
T
Abdullah. 5a657129f0 Split website stepper into home-stepper and product-stepper sections (#22149)
Reorganizes the flat `sections/stepper/` (30 files — two distinct
steppers plus shared code) into two product-feature-style sections, and
moves the shared code to the shared layers.

- **`sections/home-stepper/`** — the home-page stepper. Renamed
`Stepper` → `HomeStepper` (and the home components → `HomeStepperLottie`
/ `HomeStepperSteps` / `HomeStepperVisualFrame`) for symmetry with
`ProductStepper`. Shell at the root + `components/`/`data/`/`utils/` +
barrel.
- **`sections/product-stepper/`** — the product-page stepper, same
structure.

The 3 files both steppers shared can't live in a shared *section* —
`check-conventions` forbids a section importing another section. So they
moved to the shared layers:
- `StepperProgressRail`, `StepperSwipeDeck` → `ui/`
- `useBreakpointStepSync` → `platform/motion`

Both consumer pages repointed (`@/sections/home-stepper`,
`@/sections/product-stepper`); the row-gap allowlist in
`check-conventions.mjs` updated to the new paths; explanatory comments
stripped across the moved files (CSS-in-template comments and `'use
client'` kept).

Pure reorganization — no behavior change. typecheck + lint + build all
green.
2026-06-25 12:24:22 +02:00

348 lines
9.5 KiB
TypeScript

'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<string | null>(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<HTMLDivElement>,
) => {
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
setDraggingId(fieldId);
dragStartY.current = event.clientY;
};
const handleDragMove = (event: ReactPointerEvent<HTMLDivElement>) => {
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 (
<Canvas data-active={active ? '' : undefined}>
<MainCard>
<BlueHeader>
<HeaderLeft>
<DotsWhiteGlyph />
</HeaderLeft>
<HeaderCenter>
<PaintGlyph />
<HeaderTitle>Layout edition</HeaderTitle>
</HeaderCenter>
<HeaderSave>
<CheckGlyph /> Save
</HeaderSave>
</BlueHeader>
</MainCard>
<WidgetPanel>
<WidgetInner>
<WidgetTitle>Widget name</WidgetTitle>
<WidgetSectionLabel>General</WidgetSectionLabel>
<WidgetRow>
<WidgetIcon>
<FieldGlyph type="link" />
</WidgetIcon>
<WidgetLabel>URL</WidgetLabel>
<WidgetChip>qonto.com</WidgetChip>
</WidgetRow>
<WidgetRow>
<WidgetIcon>
<FieldGlyph type="user" />
</WidgetIcon>
<WidgetLabel>Account O...</WidgetLabel>
<WidgetValue>Phil Schiller</WidgetValue>
</WidgetRow>
<WidgetRow>
<WidgetIcon>
<FieldGlyph type="map" />
</WidgetIcon>
<WidgetLabel>Address</WidgetLabel>
<WidgetValue>18 Rue De Navarin, 750...</WidgetValue>
</WidgetRow>
<WidgetRow>
<WidgetIcon>
<FieldGlyph type="target" />
</WidgetIcon>
<WidgetLabel>ICP</WidgetLabel>
<WidgetValue> True</WidgetValue>
</WidgetRow>
</WidgetInner>
</WidgetPanel>
<NavPanel>
<NavSectionLabel>Workspace</NavSectionLabel>
{LAYOUT_EDITOR_CONTENT.navItems.map((item) => (
<div key={item.label}>
<NavItem data-active={item.isActive ? '' : undefined}>
<NavIconBox
$tint={PRODUCT_STEPPER_SCENE.navTints[item.background]}
>
<NavGlyph type={item.icon} />
</NavIconBox>
{item.label}
{item.suffix ? <NavSuffix>· {item.suffix}</NavSuffix> : null}
{item.isFolder ? <NavChevron></NavChevron> : null}
</NavItem>
{item.children?.map((child) => (
<NavSubItem key={child.label}>
<NavBreadcrumb />
<NavIconBox
$tint={PRODUCT_STEPPER_SCENE.navTints[child.background]}
>
<NavGlyph type={child.icon} />
</NavIconBox>
{child.label}
</NavSubItem>
))}
</div>
))}
</NavPanel>
<ActionsBar>
<ActionButton>+ New record</ActionButton>
<ActionButton> Enrich</ActionButton>
<ActionButton> Edit actions</ActionButton>
</ActionsBar>
<RightPanel
onLostPointerCapture={handleLostCapture}
onPointerCancel={handleDragEnd}
onPointerMove={handleDragMove}
onPointerUp={handleDragEnd}
>
<PanelHeader>
<PanelBackButton>
<ChevronLeftGlyph />
</PanelBackButton>
<PanelIconBox>
<ListGlyph />
</PanelIconBox>
<PanelTitleGroup>
<PanelTitleBold>Fields</PanelTitleBold>
<PanelTitleSub>Fields widget</PanelTitleSub>
</PanelTitleGroup>
<PanelActionButton>
<SparkGlyph />
</PanelActionButton>
</PanelHeader>
<PanelSubBar>
<PanelBackButton>
<ChevronLeftGlyph />
</PanelBackButton>
<PanelSubLabel>Layout</PanelSubLabel>
</PanelSubBar>
<PanelFields>
{sections.map((section, sectionNumber) => (
<div key={section}>
<SectionRow>
<GripGlyph />
<SectionName>{section}</SectionName>
<PanelActionButton>
<DotsGlyph />
</PanelActionButton>
</SectionRow>
{sectionNumber === 0 ? (
<EditableRow>
<EditableText>Industry</EditableText>
<DoneButton>Done</DoneButton>
</EditableRow>
) : null}
{fields
.filter((field) => field.section === section)
.map((field) => (
<FieldRow
data-dragging={draggingId === field.id ? '' : undefined}
key={field.id}
onPointerDown={(event) => handleDragStart(field.id, event)}
>
<FieldIconBox>
<FieldGlyph type={field.icon} />
</FieldIconBox>
<FieldLabels>
<FieldName>{field.label}</FieldName>
<FieldDot>·</FieldDot>
<FieldType>{field.type}</FieldType>
</FieldLabels>
<PanelActionButton
onClick={(event) => {
event.stopPropagation();
toggleVisibility(field.id);
}}
>
<EyeGlyph visible={field.visible} />
</PanelActionButton>
<PanelActionButton>
<DotsGlyph />
</PanelActionButton>
</FieldRow>
))}
{sectionNumber > 0 && sectionNumber < sections.length - 1 ? (
<AddSectionRow>
<AddIconBox>
<NewSectionGlyph />
</AddIconBox>
<AddText>Add a Section</AddText>
</AddSectionRow>
) : null}
</div>
))}
<NewFieldsRow>
<AddIconBox>
<PlusGlyph />
</AddIconBox>
<NewFieldsColumn>
<NewFieldsTitle>New fields</NewFieldsTitle>
<NewFieldsDescription>
Default position/visibility for field
</NewFieldsDescription>
</NewFieldsColumn>
</NewFieldsRow>
<AddSectionRow>
<AddIconBox>
<NewSectionGlyph />
</AddIconBox>
<AddText>Add a Section</AddText>
</AddSectionRow>
</PanelFields>
</RightPanel>
</Canvas>
);
}