import { Handshake, Heading, Image, Images, LayoutGrid, Mail, Ruler, Type, type LucideIcon } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; /** * Choosing what to put in a column. * * ── One option is still a choice worth showing ── * * I had argued the opposite: Free Text is the only kind that works, so opening the editor straight away * saves a click that asks to be got past. That was wrong in the way a shortcut usually is — it optimised * the state of the app today and hid the shape of it. Somebody adding a component cannot see what a * component IS, or that there will be others, and the day the second kind lands the interaction changes * underneath them. * * It is also the pattern this admin already uses. The nav lists unbuilt sections as `soon` rather than * hiding them, on the reasoning that a nav showing only what is finished gives no sense of what the * thing is for. The same argument applies here. * * ── The list is in the plugin, not the API ── * * The API decides what may be STORED — `free-text`, today. This decides what may be OFFERED, and it has * to live here because every kind needs a label, an icon and an editor that only exists in this plugin. * A kind the server would accept and this has never heard of is not creatable anyway. */ export type ComponentKind = | 'free-text' | 'media' | 'gallery' | 'contact-form' | 'partners' | 'collections' | 'collection-cover' | 'collection-title' | 'collection-description' | 'collection-specs' | 'collection-gallery'; /* The blocks that draw whatever the page is ABOUT. Only offered on a template. On an ordinary page they would render nothing, which is the worst kind of block: one that is available, adds cleanly, and silently does nothing. */ export const SUBJECT_KINDS: ComponentKind[] = [ 'collection-cover', 'collection-title', 'collection-description', 'collection-specs', 'collection-gallery', ]; type Choice = { kind: ComponentKind | null; label: string; hint: string; Icon: LucideIcon; }; /** * `kind: null` means not built. * * Named rather than omitted, and the ones named are things the site already HAS — media, video and * galleries all exist as concepts here. That is the difference between saying what is coming and * inventing a roadmap. Gallery stopped being one of them on 2026-08-31: the set, its ordering and the * carousel that draws it were all already built, so the block is a reference to one. */ export const CHOICES: Choice[] = [ { kind: 'free-text', label: 'Text', hint: 'Words, headings and links', Icon: Type }, /* One entry, either kind, because the block does not store which. Image and Video were two rows — one of them greyed out as `soon` — and the distinction was never the editor's to make: the path names a real file and what that file IS is a fact about it. So the picker asks for a file and the site works out the rest, which is also what a gallery item has always done. */ { kind: 'media', label: 'Media', hint: 'A photograph or a video, from the media store', Icon: Image }, { kind: 'gallery', label: 'Gallery', hint: 'A named set of media', Icon: Images }, /* Three that place something the site already draws. Nothing to choose and nothing to type, so adding one is a single click and there is no editor behind it. They are here rather than hidden because a page is built by picking blocks, and a block somebody cannot find is a block that does not exist. `collections` is the first whose content is a QUERY rather than a shared list somebody maintains — it holds nothing at all, so a collection added tomorrow appears wherever one of these sits without anybody opening a page. The hint says "every" for that reason: it is not a selection. */ { kind: 'contact-form', label: 'Contact form', hint: 'The site\'s form, as it is', Icon: Mail }, { kind: 'partners', label: 'Partners', hint: 'The partner logos, from Site', Icon: Handshake }, { kind: 'collections', label: 'Collections', hint: 'Every collection, as a grid', Icon: LayoutGrid }, /* The four that draw THIS collection, on the template. They hold nothing — not even a reference. The subject is the URL, so one block draws a different collection on every request, and that is what lets one arrangement serve all of them. */ { kind: 'collection-cover', label: 'Its lead photograph', hint: 'The first in its gallery, large', Icon: Image }, { kind: 'collection-title', label: 'Its name', hint: 'The collection this page is about', Icon: Heading }, { kind: 'collection-description', label: 'Its description', hint: 'What was written about it', Icon: Type }, { kind: 'collection-specs', label: 'Its specs', hint: 'Measurement, material, colours, delivery', Icon: Ruler }, { kind: 'collection-gallery', label: 'Its photographs', hint: 'The gallery it points at', Icon: Images }, ]; export const AddComponentDialog = ({ open, onCancel, onChoose, template, }: { open: boolean; onCancel: () => void; onChoose: (kind: ComponentKind) => void; /** * Whether this page is a TEMPLATE, which decides whether the subject blocks are offered. * * Hidden rather than disabled elsewhere. A greyed-out row invites the question "why not here", and * the answer — this page has no subject — is not something a tooltip fixes. On a page that has one * they are the point; on any other they would render nothing at all. */ template?: boolean; }) => ( !next && onCancel()}> Add a component
{/* `kind === null` is a choice that is not built yet — never a subject block, and it must not be filtered out by a list that only knows about the ones that are. */} {CHOICES.filter((choice) => template || choice.kind === null || !SUBJECT_KINDS.includes(choice.kind)).map( ({ kind, label, hint, Icon }) => ( ), )}
);