blocks nest under their band, with a button per column
Adding a block means choosing which column it goes in, so a flat list cannot express it. A one-column band shows one button and a two-column band shows two — and a band with one column never has to answer a question it has no answer to. The blocks are links, like everywhere else: pressing one opens its editor. That is the last of the four actions the painted-on bars used to carry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -920,6 +920,8 @@ export const PageBuilderPanel = () => {
|
|||||||
onAdd={() => handlersRef.current.add()}
|
onAdd={() => handlersRef.current.add()}
|
||||||
onDelete={(section) => handlersRef.current.remove(section.id)}
|
onDelete={(section) => handlersRef.current.remove(section.id)}
|
||||||
onReorder={(ids) => void reorderRef.current(ids)}
|
onReorder={(ids) => void reorderRef.current(ids)}
|
||||||
|
onAddComponent={(section, column) => handlersRef.current.addComponent(section, column)}
|
||||||
|
onDeleteComponent={(block) => handlersRef.current.removeComponent(block)}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { GripVertical, Plus, Trash2 } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { href } from '../routes';
|
import { href } from '../routes';
|
||||||
import type { CanvasSection } from '../canvas';
|
import type { CanvasComponent, CanvasSection } from '../canvas';
|
||||||
|
import { CHOICES } from './AddComponentDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page, as a list of its bands.
|
* The page, as a list of its bands.
|
||||||
@@ -31,6 +32,8 @@ export const PageStructure = ({
|
|||||||
onAdd,
|
onAdd,
|
||||||
onDelete,
|
onDelete,
|
||||||
onReorder,
|
onReorder,
|
||||||
|
onAddComponent,
|
||||||
|
onDeleteComponent,
|
||||||
}: {
|
}: {
|
||||||
/** For the links: pressing a band still takes you to it. */
|
/** For the links: pressing a band still takes you to it. */
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -39,6 +42,9 @@ export const PageStructure = ({
|
|||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onDelete: (section: CanvasSection) => void;
|
onDelete: (section: CanvasSection) => void;
|
||||||
onReorder: (ids: number[]) => void;
|
onReorder: (ids: number[]) => void;
|
||||||
|
/** A block needs a COLUMN to go in, which is the whole reason blocks are nested here. */
|
||||||
|
onAddComponent: (section: CanvasSection, column: number) => void;
|
||||||
|
onDeleteComponent: (component: CanvasComponent) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [dragged, setDragged] = useState<number | null>(null);
|
const [dragged, setDragged] = useState<number | null>(null);
|
||||||
const [order, setOrder] = useState<CanvasSection[] | null>(null);
|
const [order, setOrder] = useState<CanvasSection[] | null>(null);
|
||||||
@@ -82,10 +88,11 @@ export const PageStructure = ({
|
|||||||
key={section.id}
|
key={section.id}
|
||||||
onDragOver={(event) => moveOver(event, section.id)}
|
onDragOver={(event) => moveOver(event, section.id)}
|
||||||
onDrop={commit}
|
onDrop={commit}
|
||||||
className={`flex items-center gap-1 rounded border border-border/60 ${
|
className={`flex flex-col rounded border border-border/60 ${
|
||||||
dragged === section.id ? 'opacity-40' : ''
|
dragged === section.id ? 'opacity-40' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<span
|
<span
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
@@ -119,6 +126,56 @@ export const PageStructure = ({
|
|||||||
>
|
>
|
||||||
<Trash2 className="size-3.5" />
|
<Trash2 className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
The band's columns, and what is in each.
|
||||||
|
|
||||||
|
Nested rather than listed flat, because ADDING a block means choosing which column it
|
||||||
|
goes in — and a one-column band should not have to answer a question it has no answer
|
||||||
|
to. So a band with one column shows one button and a band with two shows two.
|
||||||
|
|
||||||
|
The blocks themselves are links, like everywhere else: pressing one opens its editor.
|
||||||
|
*/}
|
||||||
|
<div className="flex flex-col gap-1 border-t border-border/60 px-2 py-1.5">
|
||||||
|
{Array.from({ length: section.layout === 'two' ? 2 : 1 }, (_, column) => (
|
||||||
|
<div key={column} className="flex flex-col gap-0.5">
|
||||||
|
{section.components
|
||||||
|
.filter((block) => block.column === column)
|
||||||
|
.map((block) => (
|
||||||
|
<span key={block.id} className="flex items-center gap-1">
|
||||||
|
<Link
|
||||||
|
to={href('pages', slug, String(section.id), String(block.id))}
|
||||||
|
replace
|
||||||
|
className="min-w-0 flex-1 truncate py-0.5 pl-3 text-[11px] text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{CHOICES.find((choice) => choice.kind === block.kind)?.label ?? block.kind}
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 shrink-0 px-1 text-destructive"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => onDeleteComponent(block)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 justify-start px-1 text-[11px] text-muted-foreground"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => onAddComponent(section, column)}
|
||||||
|
>
|
||||||
|
<Plus className="size-3" />
|
||||||
|
{section.layout === 'two' ? (column === 0 ? 'Add left' : 'Add right') : 'Add block'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user