From 8ab0a4a516e436f955e60181de1cb8403a21e016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 31 Aug 2026 05:45:21 +0100 Subject: [PATCH] Add component asks what kind, even with one to choose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It opened the text editor straight away, and I had argued for that: Free Text is the only kind that works, so a picker offering it is a click asking to be got past. That was wrong in the way a shortcut usually is. It optimised the state of the app today and hid its shape — somebody adding a component could not 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, because a list of only what is finished gives no sense of what the thing is for. The same argument applied here and I had not noticed. Image, Video and Gallery are named and disabled. Those three are things the site already HAS — media, video and galleries all exist as concepts here — which is the difference between saying what is coming and inventing a roadmap. The list lives in the plugin rather than the API. The API decides what may be STORED; this decides what may be OFFERED, and it has to be 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. Co-Authored-By: Claude Opus 5 (1M context) --- web/sections/AddComponentDialog.tsx | 92 +++++++++++++++++++++++++++++ web/sections/PageBuilderPanel.tsx | 23 ++++++-- 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 web/sections/AddComponentDialog.tsx diff --git a/web/sections/AddComponentDialog.tsx b/web/sections/AddComponentDialog.tsx new file mode 100644 index 0000000..f90470e --- /dev/null +++ b/web/sections/AddComponentDialog.tsx @@ -0,0 +1,92 @@ +import { Images, Type, Video, 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'; + +type Choice = { + kind: ComponentKind | null; + label: string; + hint: string; + Icon: LucideIcon; +}; + +/** + * `kind: null` means not built. + * + * Named rather than omitted, and the three 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. + */ +const CHOICES: Choice[] = [ + { kind: 'free-text', label: 'Text', hint: 'Words, headings and links', Icon: Type }, + { kind: null, label: 'Image', hint: 'One picture from the media store', Icon: Images }, + { kind: null, label: 'Video', hint: 'A video, with a poster', Icon: Video }, + { kind: null, label: 'Gallery', hint: 'A named set of media', Icon: Images }, +]; + +export const AddComponentDialog = ({ + open, + onCancel, + onChoose, +}: { + open: boolean; + onCancel: () => void; + onChoose: (kind: ComponentKind) => void; +}) => ( + !next && onCancel()}> + + + Add a component + + +
+ {CHOICES.map(({ kind, label, hint, Icon }) => ( + + ))} +
+
+
+); diff --git a/web/sections/PageBuilderPanel.tsx b/web/sections/PageBuilderPanel.tsx index a55a0d5..1061299 100644 --- a/web/sections/PageBuilderPanel.tsx +++ b/web/sections/PageBuilderPanel.tsx @@ -7,6 +7,7 @@ import { usePreviewable, useViewMode } from '../ViewMode'; import { paintCanvas, type CanvasComponent, type CanvasSection } from '../canvas'; import { AddSectionDialog, type SectionLayout } from './AddSectionDialog'; import { FreeTextDialog } from './FreeTextDialog'; +import { AddComponentDialog, type ComponentKind } from './AddComponentDialog'; import { pageFromParam, usePages, type Page } from '../usePages'; /** @@ -60,6 +61,8 @@ export const PageBuilderPanel = () => { }>({ add: () => {}, remove: () => {}, addComponent: () => {}, editComponent: () => {}, removeComponent: () => {} }); const [adding, setAdding] = useState(false); const [creating, setCreating] = useState(false); + /** Where a component is about to be added, while the kind is being chosen. */ + const [choosing, setChoosing] = useState<{ sectionId: number; column: number } | null>(null); /** The component being written, and where a new one would go. Null when the dialog is shut. */ const [editing, setEditing] = useState<{ id: number | null; sectionId: number; column: number; html: string } | null>( null, @@ -177,14 +180,22 @@ export const PageBuilderPanel = () => { // Assigned on every render, after both functions above exist, so the ref always holds the current // pair without either being part of `onDocument`'s identity. /* - Free Text is the only kind, so Add component opens the editor straight away. + Add component asks WHAT, even though only one kind can be chosen today. - No picker in between: one option is not a choice, and a dialog that offers it is a click asking to - be got past. The picker arrives with the second kind, which is the moment it starts meaning - something. + It opened the text editor straight away, on the reasoning that one option is not a choice. That + optimised the state of the app rather than its shape: somebody adding a component could not see what + a component is or that there will be others, and the day the second kind lands the interaction + changes underneath them. The picker names the ones that are coming, the way the nav does. */ const addComponent = (section: CanvasSection, column: number) => - setEditing({ id: null, sectionId: section.id, column, html: '' }); + setChoosing({ sectionId: section.id, column }); + + const chooseKind = (kind: ComponentKind) => { + if (!choosing) return; + setChoosing(null); + // Only one kind reaches here today. When there are more, this is where each one's editor opens. + if (kind === 'free-text') setEditing({ id: null, ...choosing, html: '' }); + }; const editComponent = (component: CanvasComponent) => { const owner = sections.find((section) => section.components.some((one) => one.id === component.id)); @@ -263,6 +274,8 @@ export const PageBuilderPanel = () => { onCreate={(values) => void createSection(values)} /> + setChoosing(null)} onChoose={chooseKind} /> +