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;
+}) => (
+
+);
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} />
+