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);
setConfigValues(getDefaultConfigValues(definition, initialConfig));
setTitle(initialConfig.title || definition.name);
setTitle(initialConfig.title ?? '');
setDescription(initialConfig.description || '');
}, [initialConfig, allDefinitions]);
const handleSelectComponent = (def: ComponentDefinition) => {
setSelectedDef(def);
setConfigValues(getDefaultConfigValues(def));
setTitle(def.name);
setTitle('');
setDescription('');
};
@ -107,7 +107,7 @@ export function BoardComponentSelect({
const config: BoardComponentConfig = {
type: selectedDef.type,
title: title || selectedDef.name,
title,
description,
};
@ -248,7 +248,7 @@ export function BoardComponentSelect({
{t(labels.cancel)}
</Button>
<Button variant="primary" onPress={handleAdd} isDisabled={!selectedDef}>
{t(labels.add)}
{t(labels.save)}
</Button>
</Row>
</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 { Fragment, useEffect, useRef } from 'react';
import { Group, type GroupImperativeHandle, Panel, Separator } from 'react-resizable-panels';
import { v4 as uuid } from 'uuid';
import { useBoard } from '@/components/hooks';
import { GripHorizontal, Plus } from '@/components/icons';
import styles from './BoardEditLayout.module.css';
import { BoardEditRow } from './BoardEditRow';
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;
return (
<Group groupRef={rowGroupRef} orientation="vertical" style={{ minHeight }}>
{rows.map((row, index) => (
<Fragment key={`${row.id}:${row.size ?? 'auto'}`}>
<Panel
id={row.id}
minSize={MIN_ROW_HEIGHT}
maxSize={MAX_ROW_HEIGHT}
defaultSize={row.size != null ? `${row.size}%` : undefined}
>
<BoardEditRow
{...row}
rowId={row.id}
rowIndex={index}
rowCount={rows.length}
canEdit={!!websiteId}
onRemove={handleRemoveRow}
onMoveUp={handleMoveRowUp}
onMoveDown={handleMoveRowDown}
onRegisterRef={registerColumnGroupRef}
/>
<Box minHeight={`${minHeight}px`}>
<Group groupRef={rowGroupRef} orientation="vertical">
{rows.map((row, index) => (
<Fragment key={`${row.id}:${row.size ?? 'auto'}`}>
<Panel
id={row.id}
minSize={MIN_ROW_HEIGHT}
maxSize={MAX_ROW_HEIGHT}
defaultSize={row.size != null ? `${row.size}%` : undefined}
>
<BoardEditRow
{...row}
rowId={row.id}
rowIndex={index}
rowCount={rows.length}
canEdit={!!websiteId}
onRemove={handleRemoveRow}
onMoveUp={handleMoveRowUp}
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>
{index < rows.length - 1 && (
<Separator className={styles.rowSeparator}>
<span className={styles.separatorHandle}>
<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>
)}
</Group>
</Box>
);
}

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 { Panel } from '@/components/common/Panel';
import { useBoard, useMessages } from '@/components/hooks';
import { Pencil, Plus, X } from '@/components/icons';
import type { BoardComponentConfig } from '@/lib/types';
import { getComponentDefinition } from '../boardComponentRegistry';
import styles from './BoardColumn.module.css';
import { BoardComponentRenderer } from './BoardComponentRenderer';
import { BoardComponentSelect } from './BoardComponentSelect';
@ -25,6 +34,7 @@ export function BoardEditColumn({
canRemove?: boolean;
}) {
const [showSelect, setShowSelect] = useState(false);
const [showActions, setShowActions] = useState(false);
const { board } = useBoard();
const { t, labels } = useMessages();
const websiteId = board?.parameters?.websiteId;
@ -44,7 +54,7 @@ export function BoardEditColumn({
const hasComponent = !!component;
const canRemoveAction = hasComponent || canRemove;
const defaultTitle = component ? getComponentDefinition(component.type)?.name : undefined;
const title = component?.title || defaultTitle;
const title = component?.title ?? defaultTitle;
const description = component?.description;
const handleRemove = () => {
@ -62,16 +72,11 @@ export function BoardEditColumn({
width="100%"
height="100%"
position="relative"
className={styles.column}
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
>
{canEdit && canRemoveAction && (
<Box
className={styles.columnAction}
position="absolute"
top="22px"
right="24px"
zIndex={100}
>
{canEdit && canRemoveAction && showActions && (
<Box position="absolute" top="22px" right="24px" zIndex={100}>
<Row gap="2">
{hasComponent && (
<TooltipTrigger delay={0}>
@ -100,17 +105,13 @@ export function BoardEditColumn({
</Box>
) : (
canEdit && (
<Box
width="100%"
height="100%"
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<Column width="100%" height="100%" alignItems="center" justifyContent="center">
<Button variant="outline" onPress={() => setShowSelect(true)}>
<Icon>
<Plus />
</Icon>
</Button>
</Box>
</Column>
)
)}
<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 { Fragment } from 'react';
import { Fragment, useState } from 'react';
import {
Group,
type GroupImperativeHandle,
@ -12,7 +12,6 @@ import { useBoard } from '@/components/hooks';
import { ChevronDown, GripVertical, Minus, Plus } from '@/components/icons';
import type { BoardColumn as BoardColumnType, BoardComponentConfig } from '@/lib/types';
import { BoardEditColumn } from './BoardEditColumn';
import styles from './BoardEditLayout.module.css';
import { MAX_COLUMNS, MIN_COLUMN_WIDTH } from './boardConstants';
export function BoardEditRow({
@ -37,6 +36,7 @@ export function BoardEditRow({
onRegisterRef: (rowId: string, ref: GroupImperativeHandle | null) => void;
}) {
const { board, updateBoard } = useBoard();
const [showActions, setShowActions] = useState(false);
const handleGroupRef = (ref: GroupImperativeHandle | null) => {
onRegisterRef(rowId, ref);
@ -82,35 +82,68 @@ export function BoardEditRow({
};
return (
<Group groupRef={handleGroupRef} className={styles.rowGroup}>
{columns?.map((column, index) => (
<Fragment key={`${column.id}:${column.size ?? 'auto'}`}>
<ResizablePanel
id={column.id}
minSize={MIN_COLUMN_WIDTH}
defaultSize={column.size != null ? `${column.size}%` : undefined}
>
<BoardEditColumn
{...column}
canEdit={canEdit}
onRemove={handleRemoveColumn}
onSetComponent={handleSetComponent}
canRemove={columns.length > 1}
/>
</ResizablePanel>
{index < columns.length - 1 && (
<Separator className={styles.columnSeparator}>
<span className={styles.separatorHandle}>
<Icon size="sm">
<GripVertical />
</Icon>
</span>
</Separator>
)}
</Fragment>
))}
{canEdit && (
<Column className={styles.rowActions} padding="3" gap="1">
<Box
position="relative"
height="100%"
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
>
<Group groupRef={handleGroupRef}>
{columns?.map((column, index) => (
<Fragment key={`${column.id}:${column.size ?? 'auto'}`}>
<ResizablePanel
id={column.id}
minSize={MIN_COLUMN_WIDTH}
defaultSize={column.size != null ? `${column.size}%` : undefined}
>
<BoardEditColumn
{...column}
canEdit={canEdit}
onRemove={handleRemoveColumn}
onSetComponent={handleSetComponent}
canRemove={columns.length > 1}
/>
</ResizablePanel>
{index < columns.length - 1 && (
<Separator
style={{
width: '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: '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}>
<Button variant="outline" onPress={() => onMoveUp(rowId)} isDisabled={rowIndex === 0}>
<Icon rotate={180}>
@ -153,6 +186,6 @@ export function BoardEditRow({
</TooltipTrigger>
</Column>
)}
</Group>
</Box>
);
}

View file

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

View file

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

View file

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