a band shows its blocks, from one shared list

Selecting a band showed its name and title and nothing about what was in it — so
adding a block to the thing you were looking at meant going back out to the page.

BandBlocks is that list, used by the band's editor and nested under every band in
the page's structure. Same list, same actions, one component; the galleries panel
taught that lesson once already.

Blocks reorder within a column now. Across columns is the same call — the write
carries the column — but a list per column has no gesture for crossing between
them, and inventing one here would be guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 22:49:46 +01:00
co-authored by Claude Opus 5
parent a922c6fda2
commit 6a86c69890
4 changed files with 198 additions and 49 deletions
+12 -48
View File
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Link } from 'react-router';
import { href } from '../routes';
import type { CanvasComponent, CanvasSection } from '../canvas';
import { CHOICES } from './AddComponentDialog';
import { BandBlocks } from './BandBlocks';
/**
* The page, as a list of its bands.
@@ -34,6 +34,7 @@ export const PageStructure = ({
onReorder,
onAddComponent,
onDeleteComponent,
onReorderComponents,
}: {
/** For the links: pressing a band still takes you to it. */
slug: string;
@@ -45,6 +46,7 @@ export const PageStructure = ({
/** 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;
onReorderComponents: (section: CanvasSection, column: number, ids: number[]) => void;
}) => {
const [dragged, setDragged] = useState<number | null>(null);
const [order, setOrder] = useState<CanvasSection[] | null>(null);
@@ -128,53 +130,15 @@ export const PageStructure = ({
</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 className="border-t border-border/60 px-2 py-1.5">
<BandBlocks
slug={slug}
section={section}
busy={busy}
onAdd={onAddComponent}
onDelete={onDeleteComponent}
onReorder={onReorderComponents}
/>
</div>
</li>
))}