feat(website): rework the product-stepper Layout visual (#22249)

## What

Reworks the **"Layout" step** visual of the product-page stepper (the
floating record-layout-editor scene) to match the Figma references and
read as a premium, layered composition. Data is unchanged — this is
purely the look.

## Figma alignment

- **Fields editor** — the inline-edit field row now keeps the field icon
+ a bordered input + blue **Done** + the eye / ⋮ (instead of a floating
box); **"New fields"** renders as a uniform field row; section headers
get an overflow menu; field types read `Boolean` / `Date & Time`.
Dropped the redundant "Fields widget" header label.
- **"General" widget** — header chevron, the real `anonymousFelix`
avatar on Account Owner (reusing the shared asset the other
product-feature visuals use), a **Revenue** row, and a money-bag icon
for the currency field.

## Premium composition

- The nav, record, and Fields editor read as one elevated **z-stack**
(back → front): the nav is pushed right so its edge tucks under the
record (lowest z-index), the record sits above it, and the editor stays
in front — each casting a progressively stronger shadow so the depth
ordering is unmistakable.
- The **"New record / Enrich / Edit actions"** bar is centered over the
nav–record seam (`translateX(-50%)`) to tie the layered stack together
at the top.

## Testing

- typecheck · oxfmt · oxlint · check-conventions — green.
- Verified visually with headless Playwright on `/product` (Layout
step).


<img width="697" height="734" alt="image"
src="https://github.com/user-attachments/assets/58ae7ffb-13d1-4310-bbdc-864589f8bc43"
/>
This commit is contained in:
Abdullah.
2026-06-27 20:41:34 +05:00
committed by GitHub
parent 2a21eb46c0
commit 77c84815ef
5 changed files with 161 additions and 118 deletions
@@ -8,6 +8,7 @@ import {
type PointerEvent as ReactPointerEvent,
} from 'react';
import { sharedAssetUrls } from '@/app-preview/data/shared-asset-urls';
import { getElementScale } from '@/platform/motion';
import { PRODUCT_STEPPER_SCENE } from '@/tokens/feature-scenes/product-stepper-scene';
@@ -17,6 +18,7 @@ import { LAYOUT_CHROME } from './components/layout-chrome';
import { LAYOUT_EDITOR_CONTENT } from './data/layout-data';
const {
ChevronDown: ChevronDownGlyph,
ChevronLeft: ChevronLeftGlyph,
Dots: DotsGlyph,
Eye: EyeGlyph,
@@ -24,8 +26,8 @@ const {
Grip: GripGlyph,
List: ListGlyph,
Nav: NavGlyph,
NewFields: NewFieldsGlyph,
NewSection: NewSectionGlyph,
Plus: PlusGlyph,
Spark: SparkGlyph,
} = LAYOUT_GLYPHS;
@@ -36,8 +38,8 @@ const {
AddSectionRow,
AddText,
DoneButton,
EditableRow,
EditableText,
EditField,
EditGroup,
FieldDot,
FieldIconBox,
FieldLabels,
@@ -51,10 +53,6 @@ const {
NavPanel,
NavSectionLabel,
NavSubItem,
NewFieldsColumn,
NewFieldsDescription,
NewFieldsRow,
NewFieldsTitle,
PanelActionButton,
PanelBackButton,
PanelFields,
@@ -64,10 +62,10 @@ const {
PanelSubLabel,
PanelTitleBold,
PanelTitleGroup,
PanelTitleSub,
RightPanel,
SectionName,
SectionRow,
WidgetAvatar,
WidgetChip,
WidgetIcon,
WidgetInner,
@@ -158,7 +156,10 @@ export function LayoutVisual({ active }: { active: boolean }) {
>
<WidgetPanel>
<WidgetInner>
<WidgetSectionLabel>{i18n._(msg`General`)}</WidgetSectionLabel>
<WidgetSectionLabel>
{i18n._(msg`General`)}
<ChevronDownGlyph />
</WidgetSectionLabel>
<WidgetRow>
<WidgetIcon>
<FieldGlyph type="link" />
@@ -171,7 +172,14 @@ export function LayoutVisual({ active }: { active: boolean }) {
<FieldGlyph type="user" />
</WidgetIcon>
<WidgetLabel>{i18n._(msg`Account Owner`)}</WidgetLabel>
<WidgetValue>Félix Malfait</WidgetValue>
<WidgetValue>
<WidgetAvatar
alt=""
fetchPriority="low"
src={sharedAssetUrls.peopleAvatars.anonymousFelix}
/>
Félix Malfait
</WidgetValue>
</WidgetRow>
<WidgetRow>
<WidgetIcon>
@@ -187,6 +195,13 @@ export function LayoutVisual({ active }: { active: boolean }) {
<WidgetLabel>{i18n._(msg`ICP`)}</WidgetLabel>
<WidgetValue> True</WidgetValue>
</WidgetRow>
<WidgetRow>
<WidgetIcon>
<FieldGlyph type="money" />
</WidgetIcon>
<WidgetLabel>{i18n._(msg`Revenue`)}</WidgetLabel>
<WidgetValue>$500,000</WidgetValue>
</WidgetRow>
</WidgetInner>
</WidgetPanel>
@@ -253,7 +268,6 @@ export function LayoutVisual({ active }: { active: boolean }) {
</PanelIconBox>
<PanelTitleGroup>
<PanelTitleBold>{i18n._(msg`Fields`)}</PanelTitleBold>
<PanelTitleSub>{i18n._(msg`Fields widget`)}</PanelTitleSub>
</PanelTitleGroup>
<PanelActionButton>
<SparkGlyph />
@@ -275,46 +289,59 @@ export function LayoutVisual({ active }: { active: boolean }) {
<SectionName>
{i18n._(LAYOUT_EDITOR_CONTENT.sectionLabels[section])}
</SectionName>
<PanelActionButton>
<DotsGlyph />
</PanelActionButton>
</SectionRow>
{sectionNumber === 0 ? (
<EditableRow>
<EditableText>{i18n._(msg`Industry`)}</EditableText>
<DoneButton>{i18n._(msg`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>{i18n._(field.label)}</FieldName>
<FieldDot>·</FieldDot>
<FieldType>{i18n._(field.type)}</FieldType>
</FieldLabels>
<PanelActionButton
onClick={(event) => {
event.stopPropagation();
toggleVisibility(field.id);
}}
.map((field, fieldNumber) => {
const isEditing =
sectionNumber === 0 && fieldNumber === 0;
return (
<FieldRow
data-dragging={
draggingId === field.id ? '' : undefined
}
data-editing={isEditing ? '' : undefined}
key={field.id}
onPointerDown={
isEditing
? undefined
: (event) => handleDragStart(field.id, event)
}
>
<EyeGlyph visible={field.visible} />
</PanelActionButton>
<PanelActionButton>
<DotsGlyph />
</PanelActionButton>
</FieldRow>
))}
<FieldIconBox>
<FieldGlyph type={field.icon} />
</FieldIconBox>
{isEditing ? (
<EditGroup>
<EditField>{i18n._(msg`General`)}</EditField>
<DoneButton>{i18n._(msg`Done`)}</DoneButton>
</EditGroup>
) : (
<FieldLabels>
<FieldName>{i18n._(field.label)}</FieldName>
<FieldDot>·</FieldDot>
<FieldType>{i18n._(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>
@@ -327,17 +354,24 @@ export function LayoutVisual({ active }: { active: boolean }) {
</div>
))}
<NewFieldsRow>
<AddIconBox>
<PlusGlyph />
</AddIconBox>
<NewFieldsColumn>
<NewFieldsTitle>{i18n._(msg`New fields`)}</NewFieldsTitle>
<NewFieldsDescription>
<FieldRow data-static="">
<FieldIconBox>
<NewFieldsGlyph />
</FieldIconBox>
<FieldLabels>
<FieldName>{i18n._(msg`New fields`)}</FieldName>
<FieldDot>·</FieldDot>
<FieldType>
{i18n._(msg`Default position/visibility for field…`)}
</NewFieldsDescription>
</NewFieldsColumn>
</NewFieldsRow>
</FieldType>
</FieldLabels>
<PanelActionButton>
<EyeGlyph visible />
</PanelActionButton>
<PanelActionButton>
<DotsGlyph />
</PanelActionButton>
</FieldRow>
<AddSectionRow>
<AddIconBox>
@@ -2,8 +2,8 @@ import {
IconBuildingSkyscraper,
IconCalendar,
IconCheckbox,
IconChevronDown,
IconChevronLeft,
IconCurrencyDollar,
IconDotsVertical,
IconEye,
IconEyeOff,
@@ -13,8 +13,10 @@ import {
IconLink,
IconList,
IconMapPin,
IconMoneybag,
IconNewSection,
IconNotes,
IconPlus,
IconRowInsertBottom,
IconSettingsAutomation,
IconSparkles,
IconTargetArrow,
@@ -35,7 +37,7 @@ const FIELD_ICONS: Record<LayoutFieldIconType, typeof IconLink> = {
calendar: IconCalendar,
link: IconLink,
map: IconMapPin,
money: IconCurrencyDollar,
money: IconMoneybag,
target: IconTargetArrow,
user: IconUserCircle,
users: IconUsers,
@@ -71,6 +73,10 @@ function EyeGlyph({ visible }: { visible: boolean }) {
);
}
function ChevronDownGlyph() {
return <IconChevronDown color={inks.eyeInk} size={10} stroke={1.6} />;
}
function ChevronLeftGlyph() {
return <IconChevronLeft color={inks.eyeInk} size={12} stroke={1.6} />;
}
@@ -92,14 +98,15 @@ function SparkGlyph() {
}
function NewSectionGlyph() {
return <IconPlus color={inks.fieldInk} size={11} stroke={1.6} />;
return <IconNewSection color={inks.fieldInk} size={11} stroke={1.6} />;
}
function PlusGlyph() {
return <IconPlus color={inks.fieldInk} size={11} stroke={1.6} />;
function NewFieldsGlyph() {
return <IconRowInsertBottom color={inks.fieldInk} size={11} stroke={1.6} />;
}
export const LAYOUT_GLYPHS = {
ChevronDown: ChevronDownGlyph,
ChevronLeft: ChevronLeftGlyph,
Dots: DotsGlyph,
Eye: EyeGlyph,
@@ -107,7 +114,7 @@ export const LAYOUT_GLYPHS = {
Grip: GripGlyph,
List: ListGlyph,
Nav: NavGlyph,
NewFields: NewFieldsGlyph,
NewSection: NewSectionGlyph,
Plus: PlusGlyph,
Spark: SparkGlyph,
};
@@ -7,18 +7,26 @@ const layout = PRODUCT_STEPPER_SCENE.layout;
const PANEL_RADIUS = '3px';
const PANEL_SHADOW = layout.panelShadow;
const PANEL_SHADOW_MEDIUM = layout.panelShadowMedium;
const PANEL_SHADOW_ELEVATED = layout.panelShadowElevated;
const WidgetPanel = styled.div`
backdrop-filter: blur(5px);
background: ${shell.cardBackground};
border: 0.8px solid ${layout.accent};
border-radius: ${PANEL_RADIUS};
left: 48%;
box-shadow: ${PANEL_SHADOW_MEDIUM};
left: 42%;
max-height: 36%;
overflow: hidden;
padding: 6px;
position: absolute;
top: 13%;
width: 38%;
z-index: 2;
`;
const WidgetInner = styled.div`
@@ -28,9 +36,12 @@ const WidgetInner = styled.div`
`;
const WidgetSectionLabel = styled.div`
align-items: center;
color: ${shell.textTertiary};
display: flex;
font-size: 7px;
font-weight: 600;
justify-content: space-between;
padding: 0 3px;
`;
@@ -72,6 +83,15 @@ const WidgetValue = styled.span`
white-space: nowrap;
`;
const WidgetAvatar = styled.img`
border-radius: 50%;
height: 12px;
margin-right: 4px;
object-fit: cover;
vertical-align: middle;
width: 12px;
`;
const WidgetChip = styled.span`
background: ${layout.chipWash};
border: 0.5px solid ${shell.borderStrong};
@@ -86,13 +106,15 @@ const NavPanel = styled.div`
background: ${shell.cardBackground};
border: 0.8px solid ${layout.accent};
border-radius: ${PANEL_RADIUS};
left: 8%;
box-shadow: ${PANEL_SHADOW};
left: 14%;
max-height: 68%;
overflow-y: auto;
padding: 8px;
position: absolute;
top: 13%;
width: 30%;
z-index: 1;
`;
const NavSectionLabel = styled.div`
@@ -163,12 +185,14 @@ const ActionsBar = styled.div`
background: ${shell.cardBackground};
border: 0.8px solid ${layout.accent};
border-radius: ${PANEL_RADIUS};
box-shadow: ${PANEL_SHADOW};
display: flex;
gap: 5px;
left: 46%;
left: 44%;
padding: 5px 6px;
position: absolute;
top: 6%;
transform: translateX(-50%);
z-index: 3;
`;
@@ -187,12 +211,13 @@ const RightPanel = styled.div`
border: 0.8px solid ${layout.panelAccent};
border-radius: ${PANEL_RADIUS};
bottom: 3%;
box-shadow: ${PANEL_SHADOW_ELEVATED};
display: flex;
flex-direction: column;
left: 42%;
overflow: hidden;
position: absolute;
top: 36%;
top: 38%;
width: 54%;
z-index: 4;
`;
@@ -240,14 +265,6 @@ const PanelTitleBold = styled.span`
font-weight: 600;
`;
const PanelTitleSub = styled.span`
color: ${shell.textTertiary};
font-size: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const PanelSubBar = styled.div`
align-items: center;
border-bottom: 0.8px solid ${shell.borderLight};
@@ -285,21 +302,25 @@ const SectionName = styled.span`
font-weight: 600;
`;
const EditableRow = styled.div`
const EditGroup = styled.span`
align-items: center;
display: flex;
gap: 4px;
margin-right: auto;
min-width: 0;
`;
const EditField = styled.span`
background: white;
border: 1px solid ${layout.accent};
border-radius: ${PANEL_RADIUS};
display: flex;
gap: 4px;
margin: 2px 0 4px;
padding: 4px 6px;
`;
const EditableText = styled.span`
color: ${shell.text};
flex: 1;
font-size: 9px;
font-size: 8px;
overflow: hidden;
padding: 3px 6px;
text-overflow: ellipsis;
white-space: nowrap;
width: 120px;
`;
const DoneButton = styled.span`
@@ -329,6 +350,11 @@ const FieldRow = styled.div`
&:active {
cursor: grabbing;
}
&[data-editing],
&[data-static] {
cursor: default;
}
`;
const FieldIconBox = styled.span`
@@ -396,30 +422,6 @@ const AddText = styled.span`
font-size: 8px;
`;
const NewFieldsRow = styled.div`
align-items: center;
border-top: 0.8px solid ${shell.borderMedium};
display: flex;
gap: 5px;
margin-top: 4px;
padding: 6px 3px 3px;
`;
const NewFieldsColumn = styled.div`
display: flex;
flex-direction: column;
`;
const NewFieldsTitle = styled.span`
color: ${shell.textSecondary};
font-size: 8px;
`;
const NewFieldsDescription = styled.span`
color: ${shell.textTertiary};
font-size: 7px;
`;
export const LAYOUT_CHROME = {
ActionButton,
ActionsBar,
@@ -427,8 +429,8 @@ export const LAYOUT_CHROME = {
AddSectionRow,
AddText,
DoneButton,
EditableRow,
EditableText,
EditField,
EditGroup,
FieldDot,
FieldIconBox,
FieldLabels,
@@ -442,10 +444,6 @@ export const LAYOUT_CHROME = {
NavPanel,
NavSectionLabel,
NavSubItem,
NewFieldsColumn,
NewFieldsDescription,
NewFieldsRow,
NewFieldsTitle,
PanelActionButton,
PanelBackButton,
PanelFields,
@@ -455,10 +453,10 @@ export const LAYOUT_CHROME = {
PanelSubLabel,
PanelTitleBold,
PanelTitleGroup,
PanelTitleSub,
RightPanel,
SectionName,
SectionRow,
WidgetAvatar,
WidgetChip,
WidgetIcon,
WidgetInner,
@@ -38,7 +38,7 @@ export const LAYOUT_EDITOR_CONTENT: {
id: 'icp',
icon: 'target',
label: msg`ICP`,
type: msg`True/False`,
type: msg`Boolean`,
section: 'Additional',
visible: false,
},
@@ -62,7 +62,7 @@ export const LAYOUT_EDITOR_CONTENT: {
id: 'creation-date',
icon: 'calendar',
label: msg`Creation date`,
type: msg`Date and Time`,
type: msg`Date & Time`,
section: 'Other',
visible: true,
},
@@ -63,6 +63,10 @@ export const PRODUCT_STEPPER_SCENE = {
fieldInk: FICTION_PALETTE.inkMuted,
eyeInk: FICTION_PALETTE.inkFaint,
eyeHiddenInk: FICTION_PALETTE.inkDisabled,
panelShadow: '0 4px 16px rgba(0, 0, 0, 0.32)',
panelShadowMedium: '0 8px 28px rgba(0, 0, 0, 0.42)',
panelShadowElevated:
'0 12px 44px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3)',
},
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)',