From a922c6fda21f9cf375178e0d6968a2ffc78cb597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 1 Sep 2026 22:44:27 +0100 Subject: [PATCH] blocks nest under their band, with a button per column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- web/sections/PageBuilderPanel.tsx | 2 + web/sections/PageStructure.tsx | 61 ++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index 2c47456..da1f6f4 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -920,6 +920,8 @@ export const PageBuilderPanel = () => { onAdd={() => handlersRef.current.add()} onDelete={(section) => handlersRef.current.remove(section.id)} onReorder={(ids) => void reorderRef.current(ids)} + onAddComponent={(section, column) => handlersRef.current.addComponent(section, column)} + onDeleteComponent={(block) => handlersRef.current.removeComponent(block)} />, ); return; diff --git a/web/sections/PageStructure.tsx b/web/sections/PageStructure.tsx index 8524977..d5e285a 100644 --- a/web/sections/PageStructure.tsx +++ b/web/sections/PageStructure.tsx @@ -3,7 +3,8 @@ import { GripVertical, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Link } from 'react-router'; 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. @@ -31,6 +32,8 @@ export const PageStructure = ({ onAdd, onDelete, onReorder, + onAddComponent, + onDeleteComponent, }: { /** For the links: pressing a band still takes you to it. */ slug: string; @@ -39,6 +42,9 @@ export const PageStructure = ({ onAdd: () => void; onDelete: (section: CanvasSection) => 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(null); const [order, setOrder] = useState(null); @@ -82,10 +88,11 @@ export const PageStructure = ({ key={section.id} onDragOver={(event) => moveOver(event, section.id)} 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' : '' }`} > +
{ @@ -119,6 +126,56 @@ export const PageStructure = ({ > +
+ + {/* + 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. + */} +
+ {Array.from({ length: section.layout === 'two' ? 2 : 1 }, (_, column) => ( +
+ {section.components + .filter((block) => block.column === column) + .map((block) => ( + + + {CHOICES.find((choice) => choice.kind === block.kind)?.label ?? block.kind} + + + + ))} + + +
+ ))} +
))}