Migrate board layout UI to react-zen and preserve empty component titles

This commit is contained in:
Mike Cao 2026-02-13 01:53:53 -08:00
parent db637864f6
commit cda9c684c3
9 changed files with 168 additions and 177 deletions

View file

@ -1,17 +0,0 @@
.column {
position: relative;
}
.columnAction {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 120ms ease;
}
.column:hover .columnAction,
.column:focus-within .columnAction {
opacity: 1;
visibility: visible;
pointer-events: auto;
}

View file

@ -73,14 +73,14 @@ export function BoardComponentSelect({
setSelectedDef(definition); setSelectedDef(definition);
setConfigValues(getDefaultConfigValues(definition, initialConfig)); setConfigValues(getDefaultConfigValues(definition, initialConfig));
setTitle(initialConfig.title || definition.name); setTitle(initialConfig.title ?? '');
setDescription(initialConfig.description || ''); setDescription(initialConfig.description || '');
}, [initialConfig, allDefinitions]); }, [initialConfig, allDefinitions]);
const handleSelectComponent = (def: ComponentDefinition) => { const handleSelectComponent = (def: ComponentDefinition) => {
setSelectedDef(def); setSelectedDef(def);
setConfigValues(getDefaultConfigValues(def)); setConfigValues(getDefaultConfigValues(def));
setTitle(def.name); setTitle('');
setDescription(''); setDescription('');
}; };
@ -107,7 +107,7 @@ export function BoardComponentSelect({
const config: BoardComponentConfig = { const config: BoardComponentConfig = {
type: selectedDef.type, type: selectedDef.type,
title: title || selectedDef.name, title,
description, description,
}; };
@ -248,7 +248,7 @@ export function BoardComponentSelect({
{t(labels.cancel)} {t(labels.cancel)}
</Button> </Button>
<Button variant="primary" onPress={handleAdd} isDisabled={!selectedDef}> <Button variant="primary" onPress={handleAdd} isDisabled={!selectedDef}>
{t(labels.add)} {t(labels.save)}
</Button> </Button>
</Row> </Row>
</Column> </Column>

View file

@ -1,11 +1,10 @@
import { Button, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { Box, Button, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { produce } from 'immer'; import { produce } from 'immer';
import { Fragment, useEffect, useRef } from 'react'; import { Fragment, useEffect, useRef } from 'react';
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels'; import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { useBoard } from '@/components/hooks'; import { useBoard } from '@/components/hooks';
import { GripHorizontal, Plus } from '@/components/icons'; import { GripHorizontal, Plus } from '@/components/icons';
import styles from './BoardEditLayout.module.css';
import { BoardEditRow } from './BoardEditRow'; import { BoardEditRow } from './BoardEditRow';
import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants'; import { BUTTON_ROW_HEIGHT, MAX_ROW_HEIGHT, MIN_ROW_HEIGHT } from './boardConstants';
@ -108,52 +107,71 @@ export function BoardEditBody() {
const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT; const minHeight = (rows.length || 1) * MAX_ROW_HEIGHT + BUTTON_ROW_HEIGHT;
return ( return (
<Group groupRef={rowGroupRef} orientation="vertical" style={{ minHeight }}> <Box minHeight={`${minHeight}px`}>
{rows.map((row, index) => ( <Group groupRef={rowGroupRef} orientation="vertical">
<Fragment key={`${row.id}:${row.size ?? 'auto'}`}> {rows.map((row, index) => (
<Panel <Fragment key={`${row.id}:${row.size ?? 'auto'}`}>
id={row.id} <Panel
minSize={MIN_ROW_HEIGHT} id={row.id}
maxSize={MAX_ROW_HEIGHT} minSize={MIN_ROW_HEIGHT}
defaultSize={row.size != null ? `${row.size}%` : undefined} maxSize={MAX_ROW_HEIGHT}
> defaultSize={row.size != null ? `${row.size}%` : undefined}
<BoardEditRow >
{...row} <BoardEditRow
rowId={row.id} {...row}
rowIndex={index} rowId={row.id}
rowCount={rows.length} rowIndex={index}
canEdit={!!websiteId} rowCount={rows.length}
onRemove={handleRemoveRow} canEdit={!!websiteId}
onMoveUp={handleMoveRowUp} onRemove={handleRemoveRow}
onMoveDown={handleMoveRowDown} onMoveUp={handleMoveRowUp}
onRegisterRef={registerColumnGroupRef} onMoveDown={handleMoveRowDown}
/> onRegisterRef={registerColumnGroupRef}
/>
</Panel>
{index < rows.length - 1 && (
<Separator
style={{
height: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
outline: 'none',
boxShadow: 'none',
background: 'transparent',
}}
>
<Row
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
style={{ cursor: 'row-resize' }}
>
<Icon size="sm">
<GripHorizontal />
</Icon>
</Row>
</Separator>
)}
</Fragment>
))}
{!!websiteId && (
<Panel minSize={BUTTON_ROW_HEIGHT}>
<Row padding="3">
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={handleAddRow}>
<Icon>
<Plus />
</Icon>
</Button>
<Tooltip placement="bottom">Add row</Tooltip>
</TooltipTrigger>
</Row>
</Panel> </Panel>
{index < rows.length - 1 && ( )}
<Separator className={styles.rowSeparator}> </Group>
<span className={styles.separatorHandle}> </Box>
<Icon size="sm">
<GripHorizontal />
</Icon>
</span>
</Separator>
)}
</Fragment>
))}
{!!websiteId && (
<Panel minSize={BUTTON_ROW_HEIGHT}>
<Row padding="3">
<TooltipTrigger delay={0}>
<Button variant="outline" onPress={handleAddRow}>
<Icon>
<Plus />
</Icon>
</Button>
<Tooltip placement="bottom">Add row</Tooltip>
</TooltipTrigger>
</Row>
</Panel>
)}
</Group>
); );
} }

View file

@ -1,11 +1,20 @@
import { Box, Button, Dialog, Icon, Modal, Row, Tooltip, TooltipTrigger } from '@umami/react-zen'; import {
Box,
Button,
Column,
Dialog,
Icon,
Modal,
Row,
Tooltip,
TooltipTrigger,
} from '@umami/react-zen';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { useBoard, useMessages } from '@/components/hooks'; import { useBoard, useMessages } from '@/components/hooks';
import { Pencil, Plus, X } from '@/components/icons'; import { Pencil, Plus, X } from '@/components/icons';
import type { BoardComponentConfig } from '@/lib/types'; import type { BoardComponentConfig } from '@/lib/types';
import { getComponentDefinition } from '../boardComponentRegistry'; import { getComponentDefinition } from '../boardComponentRegistry';
import styles from './BoardColumn.module.css';
import { BoardComponentRenderer } from './BoardComponentRenderer'; import { BoardComponentRenderer } from './BoardComponentRenderer';
import { BoardComponentSelect } from './BoardComponentSelect'; import { BoardComponentSelect } from './BoardComponentSelect';
@ -25,6 +34,7 @@ export function BoardEditColumn({
canRemove?: boolean; canRemove?: boolean;
}) { }) {
const [showSelect, setShowSelect] = useState(false); const [showSelect, setShowSelect] = useState(false);
const [showActions, setShowActions] = useState(false);
const { board } = useBoard(); const { board } = useBoard();
const { t, labels } = useMessages(); const { t, labels } = useMessages();
const websiteId = board?.parameters?.websiteId; const websiteId = board?.parameters?.websiteId;
@ -44,7 +54,7 @@ export function BoardEditColumn({
const hasComponent = !!component; const hasComponent = !!component;
const canRemoveAction = hasComponent || canRemove; const canRemoveAction = hasComponent || canRemove;
const defaultTitle = component ? getComponentDefinition(component.type)?.name : undefined; const defaultTitle = component ? getComponentDefinition(component.type)?.name : undefined;
const title = component?.title || defaultTitle; const title = component?.title ?? defaultTitle;
const description = component?.description; const description = component?.description;
const handleRemove = () => { const handleRemove = () => {
@ -62,16 +72,11 @@ export function BoardEditColumn({
width="100%" width="100%"
height="100%" height="100%"
position="relative" position="relative"
className={styles.column} onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
> >
{canEdit && canRemoveAction && ( {canEdit && canRemoveAction && showActions && (
<Box <Box position="absolute" top="22px" right="24px" zIndex={100}>
className={styles.columnAction}
position="absolute"
top="22px"
right="24px"
zIndex={100}
>
<Row gap="2"> <Row gap="2">
{hasComponent && ( {hasComponent && (
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>
@ -100,17 +105,13 @@ export function BoardEditColumn({
</Box> </Box>
) : ( ) : (
canEdit && ( canEdit && (
<Box <Column width="100%" height="100%" alignItems="center" justifyContent="center">
width="100%"
height="100%"
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<Button variant="outline" onPress={() => setShowSelect(true)}> <Button variant="outline" onPress={() => setShowSelect(true)}>
<Icon> <Icon>
<Plus /> <Plus />
</Icon> </Icon>
</Button> </Button>
</Box> </Column>
) )
)} )}
<Modal isOpen={showSelect} onOpenChange={setShowSelect}> <Modal isOpen={showSelect} onOpenChange={setShowSelect}>

View file

@ -1,46 +0,0 @@
.columnSeparator {
width: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: col-resize;
}
.rowSeparator {
height: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: row-resize;
}
.separatorHandle {
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-9);
}
.rowGroup {
position: relative;
height: 100%;
}
.rowActions {
position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);
z-index: 20;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 120ms ease;
}
.rowGroup:hover .rowActions,
.rowGroup:focus-within .rowActions {
opacity: 1;
visibility: visible;
pointer-events: auto;
}

View file

@ -1,6 +1,6 @@
import { Button, Column, Icon, Tooltip, TooltipTrigger } from '@umami/react-zen'; import { Box, Button, Column, Icon, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
import { produce } from 'immer'; import { produce } from 'immer';
import { Fragment } from 'react'; import { Fragment, useState } from 'react';
import { import {
Group, Group,
type GroupImperativeHandle, type GroupImperativeHandle,
@ -12,7 +12,6 @@ import { useBoard } from '@/components/hooks';
import { ChevronDown, GripVertical, Minus, Plus } from '@/components/icons'; import { ChevronDown, GripVertical, Minus, Plus } from '@/components/icons';
import type { BoardColumn as BoardColumnType, BoardComponentConfig } from '@/lib/types'; import type { BoardColumn as BoardColumnType, BoardComponentConfig } from '@/lib/types';
import { BoardEditColumn } from './BoardEditColumn'; import { BoardEditColumn } from './BoardEditColumn';
import styles from './BoardEditLayout.module.css';
import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants'; import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants';
export function BoardEditRow({ export function BoardEditRow({
@ -37,6 +36,7 @@ export function BoardEditRow({
onRegisterRef: (rowId: string, ref: GroupImperativeHandle | null) => void; onRegisterRef: (rowId: string, ref: GroupImperativeHandle | null) => void;
}) { }) {
const { board, updateBoard } = useBoard(); const { board, updateBoard } = useBoard();
const [showActions, setShowActions] = useState(false);
const handleGroupRef = (ref: GroupImperativeHandle | null) => { const handleGroupRef = (ref: GroupImperativeHandle | null) => {
onRegisterRef(rowId, ref); onRegisterRef(rowId, ref);
@ -82,35 +82,68 @@ export function BoardEditRow({
}; };
return ( return (
<Group groupRef={handleGroupRef} className={styles.rowGroup}> <Box
{columns?.map((column, index) => ( position="relative"
<Fragment key={`${column.id}:${column.size ?? 'auto'}`}> height="100%"
<ResizablePanel onMouseEnter={() => setShowActions(true)}
id={column.id} onMouseLeave={() => setShowActions(false)}
minSize={MIN_COLUMN_WIDTH} >
defaultSize={column.size != null ? `${column.size}%` : undefined} <Group groupRef={handleGroupRef}>
> {columns?.map((column, index) => (
<BoardEditColumn <Fragment key={`${column.id}:${column.size ?? 'auto'}`}>
{...column} <ResizablePanel
canEdit={canEdit} id={column.id}
onRemove={handleRemoveColumn} minSize={MIN_COLUMN_WIDTH}
onSetComponent={handleSetComponent} defaultSize={column.size != null ? `${column.size}%` : undefined}
canRemove={columns.length > 1} >
/> <BoardEditColumn
</ResizablePanel> {...column}
{index < columns.length - 1 && ( canEdit={canEdit}
<Separator className={styles.columnSeparator}> onRemove={handleRemoveColumn}
<span className={styles.separatorHandle}> onSetComponent={handleSetComponent}
<Icon size="sm"> canRemove={columns.length > 1}
<GripVertical /> />
</Icon> </ResizablePanel>
</span> {index < columns.length - 1 && (
</Separator> <Separator
)} style={{
</Fragment> width: '12px',
))} display: 'flex',
{canEdit && ( alignItems: 'center',
<Column className={styles.rowActions} padding="3" gap="1"> justifyContent: 'center',
border: 'none',
outline: 'none',
boxShadow: 'none',
background: 'transparent',
}}
>
<Row
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
style={{ cursor: 'col-resize' }}
>
<Icon size="sm">
<GripVertical />
</Icon>
</Row>
</Separator>
)}
</Fragment>
))}
</Group>
{canEdit && showActions && (
<Column
padding="3"
gap="1"
position="absolute"
top="0"
bottom="0"
right="12px"
zIndex={20}
justifyContent="center"
>
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>
<Button variant="outline" onPress={() => onMoveUp(rowId)} isDisabled={rowIndex === 0}> <Button variant="outline" onPress={() => onMoveUp(rowId)} isDisabled={rowIndex === 0}>
<Icon rotate={180}> <Icon rotate={180}>
@ -153,6 +186,6 @@ export function BoardEditRow({
</TooltipTrigger> </TooltipTrigger>
</Column> </Column>
)} )}
</Group> </Box>
); );
} }

View file

@ -1,3 +1,4 @@
import { Column } from '@umami/react-zen';
import { useBoard } from '@/components/hooks'; import { useBoard } from '@/components/hooks';
import { BoardViewRow } from './BoardViewRow'; import { BoardViewRow } from './BoardViewRow';
@ -6,10 +7,10 @@ export function BoardViewBody() {
const rows = board?.parameters?.rows ?? []; const rows = board?.parameters?.rows ?? [];
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <Column gap="3">
{rows.map(row => ( {rows.map(row => (
<BoardViewRow key={row.id} columns={row.columns} /> <BoardViewRow key={row.id} columns={row.columns} />
))} ))}
</div> </Column>
); );
} }

View file

@ -13,7 +13,7 @@ export function BoardViewColumn({ component }: { component?: BoardComponentConfi
return null; return null;
} }
const title = component.title || getComponentDefinition(component.type)?.name; const title = component.title ?? getComponentDefinition(component.type)?.name;
const description = component.description; const description = component.description;
return ( return (

View file

@ -1,21 +1,22 @@
import { Box, Row } from '@umami/react-zen';
import type { BoardColumn } from '@/lib/types'; import type { BoardColumn } from '@/lib/types';
import { BoardViewColumn } from './BoardViewColumn'; import { BoardViewColumn } from './BoardViewColumn';
import { MIN_COLUMN_WIDTH } from './boardConstants'; import { MIN_COLUMN_WIDTH } from './boardConstants';
export function BoardViewRow({ columns }: { columns: BoardColumn[] }) { export function BoardViewRow({ columns }: { columns: BoardColumn[] }) {
return ( return (
<div style={{ display: 'flex', gap: 12, width: '100%', overflowX: 'auto' }}> <Row gap="3" width="100%" overflowX="auto">
{columns.map(column => ( {columns.map(column => (
<div <Box
key={column.id} key={column.id}
style={{ flexGrow={column.size ?? 1}
flex: `${column.size ?? 1} 1 0%`, flexShrink={1}
minWidth: MIN_COLUMN_WIDTH, flexBasis="0%"
}} minWidth={`${MIN_COLUMN_WIDTH}px`}
> >
<BoardViewColumn component={column.component} /> <BoardViewColumn component={column.component} />
</div> </Box>
))} ))}
</div> </Row>
); );
} }