# Editor Tabs Restructure Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Reduce the editor's four tabs to three, and move field design and card design out of tabs and onto clicks in the canvas.

**Architecture:** A pure panel-state module decides what the settings column shows: the active tab, or a takeover panel for a field, the card, or the field picker. The panels are existing components relocated, not new UI. Tabs are removed *last*, after every panel is reachable from the canvas, so the editor works at every commit.

**Tech Stack:** React 18 + TypeScript, Vite. Tests are `node --test` over esbuild bundles (`npm test`), no DOM — so logic that needs testing must live in pure modules. The builder renders inside a Web Component shadow root.

**Spec:** `docs/superpowers/specs/2026-08-31-editor-tabs-restructure-design.md`

## Global Constraints

- **No schema changes.** The JSON a host receives must be byte-identical before and after. Nothing in `src/schema/` or `src/renderer/` changes.
- **No portals.** The builder lives in a shadow root; `createPortal` to `document.body` escapes it and loses all styling. Render inline. (See `Select.tsx` `clippingBounds` for how existing code copes.)
- **No em dashes in user-facing copy.** Hebrew and English strings use commas, colons or parentheses instead. Code comments may use them.
- **Both dictionaries stay in step.** `Strings` is derived from `src/builder/i18n/en.ts`, so a key added there is required in `src/builder/i18n/he.ts` and TypeScript will fail until it exists.
- **Every task ends green:** `npm run typecheck` and `npm test` both pass before committing.
- **Branch:** `feat/editor-tabs-restructure`, already created, already rebased on `fix/default-layout-image`.

## Correction to the spec

The spec lists `היכן מופיע (URLs)` under `סוג טופס`. That is wrong: URL targeting is not in any tab. It lives in the Publish popover (`PublishPopover.tsx:200`), reached from the Publish button, and `EditingView` passes `onUrlsChange` to `PreviewPane` for it. Leave it exactly where it is. The rest of the spec is accurate.

## File structure

| File | Responsibility |
|---|---|
| `src/builder/editorPanel.ts` (new) | Pure state machine for what the settings column shows. No React. |
| `src/builder/components/EditorPanel.tsx` (new) | The takeover shell: back button, title, body. Shared by all three panels. |
| `src/builder/components/CardPanel.tsx` (new) | Card design (moved from `DesignEditor`) plus the field outline. |
| `src/builder/components/FieldPanel.tsx` (new) | One field's settings + design (moved from `ContentItemsEditor`). |
| `src/builder/components/FormTypeEditor.tsx` (new) | `סוג טופס` tab: form name + Delivery. Split out of `SettingsEditor`. |
| `src/builder/components/SettingsEditor.tsx` | Shrinks to the `הגדרות` tab: Steps + Submission. Keeps `AutomationsSection`, `DesignEditor` and the rest of its current occupants. |
| `src/builder/components/EditingView.tsx` | Three tabs; owns panel state; routes canvas clicks. |
| `src/builder/components/AddMenu.tsx` | Deleted. |
| `src/builder/components/FieldPicker.tsx` | Loses the `popover` variant. |
| `tests/editor-panel.test.ts` (new) | Covers the state machine. |

`SettingsEditor.tsx` is 1751 lines and holds five unrelated exports. This plan splits off two of them because it has to; it does not attempt a full teardown.

---

### Task 1: The panel state machine

Pure logic first, so the rest of the plan has something tested to build on. No UI changes in this task; nothing imports it yet.

**Files:**
- Create: `src/builder/editorPanel.ts`
- Test: `tests/editor-panel.test.ts`

**Interfaces:**
- Consumes: nothing.
- Produces: `type EditorPanel`, `type PanelAction`, `panelReducer(state: EditorPanel, action: PanelAction): EditorPanel`, `INITIAL_PANEL: EditorPanel`, `panelFieldId(state: EditorPanel): string | null`.

- [ ] **Step 1: Write the failing test**

Create `tests/editor-panel.test.ts`:

```ts
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import {
  panelReducer,
  INITIAL_PANEL,
  panelFieldId,
  type EditorPanel,
} from '../src/builder/editorPanel';

/**
 * What the settings column is showing.
 *
 * It is either the tab the author picked, or a panel that has taken the column
 * over: one field, the card, or the picker. The rules about which replaces
 * which are the whole of this module, and they are worth testing on their own
 * because the component that used to hold them cannot be rendered in this test
 * runner.
 */

const field = (id: string): EditorPanel => ({ kind: 'field', id });

describe('the settings column', () => {
  test('starts on the tab, with nothing taking it over', () => {
    assert.deepEqual(INITIAL_PANEL, { kind: 'none' });
  });

  test('a click on a field opens that field', () => {
    assert.deepEqual(panelReducer(INITIAL_PANEL, { type: 'canvasClick', id: 'a' }), field('a'));
  });

  test('a click on the card opens the card', () => {
    assert.deepEqual(panelReducer(INITIAL_PANEL, { type: 'canvasClick', id: null }), { kind: 'card' });
  });

  test('clicking another field swaps the panel rather than closing it', () => {
    assert.deepEqual(panelReducer(field('a'), { type: 'canvasClick', id: 'b' }), field('b'));
  });

  test('clicking the card while a field is open switches to the card', () => {
    assert.deepEqual(panelReducer(field('a'), { type: 'canvasClick', id: null }), { kind: 'card' });
  });

  test('the picker opens over whatever was showing', () => {
    assert.deepEqual(panelReducer(field('a'), { type: 'openPicker' }), { kind: 'picker' });
    assert.deepEqual(panelReducer(INITIAL_PANEL, { type: 'openPicker' }), { kind: 'picker' });
  });

  test('closing goes back to the tab', () => {
    for (const from of [field('a'), { kind: 'card' } as EditorPanel, { kind: 'picker' } as EditorPanel]) {
      assert.deepEqual(panelReducer(from, { type: 'close' }), { kind: 'none' });
    }
  });

  test('switching tabs closes any panel, since a tab is a move away', () => {
    assert.deepEqual(panelReducer(field('a'), { type: 'tabChange' }), { kind: 'none' });
    assert.deepEqual(panelReducer({ kind: 'card' }, { type: 'tabChange' }), { kind: 'none' });
  });

  test('closing when nothing is open changes nothing', () => {
    assert.equal(panelReducer(INITIAL_PANEL, { type: 'close' }), INITIAL_PANEL);
  });

  test('the selected field id is readable, and only for a field', () => {
    assert.equal(panelFieldId(field('a')), 'a');
    assert.equal(panelFieldId({ kind: 'card' }), null);
    assert.equal(panelFieldId({ kind: 'picker' }), null);
    assert.equal(panelFieldId(INITIAL_PANEL), null);
  });

  test('a field that no longer exists is dropped', () => {
    // Deleting the open field from its own panel must not leave the column
    // pointing at something the form no longer has.
    assert.deepEqual(panelReducer(field('gone'), { type: 'fieldRemoved', id: 'gone' }), { kind: 'none' });
    assert.deepEqual(panelReducer(field('a'), { type: 'fieldRemoved', id: 'other' }), field('a'));
  });
});
```

- [ ] **Step 2: Run the test and watch it fail**

Run: `npm test 2>&1 | grep -E "editor-panel|fail"`
Expected: the build step fails to resolve `../src/builder/editorPanel` — the module does not exist yet.

- [ ] **Step 3: Write the module**

Create `src/builder/editorPanel.ts`:

```ts
/**
 * What the editor's settings column is showing.
 *
 * The column has two jobs that cannot both be visible: the tab the author
 * picked, and a panel that has taken the column over to edit one thing. A
 * panel is opened by pointing at something — a field or the card in the canvas
 * — or by asking to add a field, and it is closed by going back, by pressing
 * Esc, or by moving to another tab.
 *
 * It lives here, apart from the view, because the view cannot be rendered in
 * this project's test runner (no DOM), and these rules are worth more tests
 * than a component of that size would otherwise get.
 */

export type EditorPanel =
  /** No takeover: the column is showing the active tab. */
  | { kind: 'none' }
  /** One field's settings and design. */
  | { kind: 'field'; id: string }
  /** The card's design, and the outline of what the form holds. */
  | { kind: 'card' }
  /** The field picker, which rows can be dragged out of onto the canvas. */
  | { kind: 'picker' };

export type PanelAction =
  /**
   * The canvas reported a click. `id` is the item clicked, or `null` for the
   * card itself — the renderer already sends `null` for a click on empty card
   * space (see PopupContent's `onClickCapture`), which is exactly this.
   */
  | { type: 'canvasClick'; id: string | null }
  | { type: 'openPicker' }
  | { type: 'close' }
  | { type: 'tabChange' }
  /** An item was deleted; if its panel is open it has nothing left to show. */
  | { type: 'fieldRemoved'; id: string };

export const INITIAL_PANEL: EditorPanel = { kind: 'none' };

export function panelReducer(state: EditorPanel, action: PanelAction): EditorPanel {
  switch (action.type) {
    case 'canvasClick':
      return action.id === null ? { kind: 'card' } : { kind: 'field', id: action.id };
    case 'openPicker':
      return { kind: 'picker' };
    case 'close':
    case 'tabChange':
      // Returning the same object when nothing is open keeps React from
      // re-rendering the column on every stray Esc.
      return state.kind === 'none' ? state : { kind: 'none' };
    case 'fieldRemoved':
      return state.kind === 'field' && state.id === action.id ? { kind: 'none' } : state;
  }
}

/** The field a panel is open on, or null when it isn't open on a field. */
export function panelFieldId(state: EditorPanel): string | null {
  return state.kind === 'field' ? state.id : null;
}
```

- [ ] **Step 4: Run the test and watch it pass**

Run: `npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"`
Expected: `fail 0`, and the total has risen by 11.

- [ ] **Step 5: Typecheck and commit**

```bash
npm run typecheck
git add src/builder/editorPanel.ts tests/editor-panel.test.ts
git commit -m "feat(builder): a state machine for what the settings column shows"
```

---

### Task 2: The takeover shell

One component all three panels sit inside, so back, title and scroll behave the same in each. Modelled on `FontBrowser` in `src/builder/components/FontPicker.tsx`, which already does this — read it before writing this task.

**Files:**
- Create: `src/builder/components/EditorPanel.tsx`
- Modify: `src/builder/i18n/en.ts`, `src/builder/i18n/he.ts`
- Modify: `src/builder/styles/05-base-pickers.css`

**Interfaces:**
- Consumes: nothing from Task 1.
- Produces: `<EditorPanelShell title={string} onClose={() => void}>{children}</EditorPanelShell>`.

- [ ] **Step 1: Add the strings**

In `src/builder/i18n/en.ts`, inside the `edit` group (near `tabFormType`, around line 34):

```ts
    panelBack: 'Back',
    panelCard: 'Card',
    panelFields: 'Fields in this form',
```

In `src/builder/i18n/he.ts`, inside the same group (around line 31):

```ts
    panelBack: "חזרה",
    panelCard: "הכרטיס",
    panelFields: "שדות בטופס",
```

- [ ] **Step 2: Write the shell**

Create `src/builder/components/EditorPanel.tsx`:

```tsx
import { useEffect, useRef, type ReactNode } from 'react';
import { useT } from '../i18n';

/**
 * A panel that takes the settings column over to edit one thing.
 *
 * The same shape as the font browser, and for the same reason: the column is
 * narrow, and a panel that shared it with the tab underneath would leave
 * neither enough room. It anchors to the top of the column and locks that
 * column's own scroll, so the takeover reads as one surface rather than two
 * stacked ones.
 *
 * Esc closes it. Focus moves to the heading on open so a screen reader
 * announces what just took over; it is not returned on close, because what
 * opened the panel was a click on a canvas element that is still there.
 */
export function EditorPanelShell({
  title,
  onClose,
  children,
}: {
  title: string;
  onClose: () => void;
  children: ReactNode;
}) {
  const t = useT();
  const rootRef = useRef<HTMLDivElement>(null);
  const headingRef = useRef<HTMLHeadingElement>(null);

  useEffect(() => {
    const menu = rootRef.current?.closest<HTMLElement>('.edit-menu');
    if (!menu) return;
    const prevScroll = menu.scrollTop;
    const prevOverflow = menu.style.overflow;
    menu.scrollTop = 0;
    menu.style.overflow = 'hidden';
    return () => {
      menu.style.overflow = prevOverflow;
      menu.scrollTop = prevScroll;
    };
  }, []);

  useEffect(() => {
    headingRef.current?.focus();
  }, []);

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div className="editor-panel" ref={rootRef} role="region" aria-label={title}>
      <header className="editor-panel-head">
        <button
          type="button"
          className="editor-panel-back"
          onClick={onClose}
          aria-label={t.edit.panelBack}
        >
          <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
            <path d="M15 18l-6-6 6-6" className="chevron-ltr" />
          </svg>
        </button>
        {/* tabIndex -1 so focus can be moved here without adding a tab stop. */}
        <h2 className="editor-panel-title" ref={headingRef} tabIndex={-1}>{title}</h2>
      </header>
      <div className="editor-panel-body">{children}</div>
    </div>
  );
}
```

- [ ] **Step 3: Add the styles**

Append to `src/builder/styles/05-base-pickers.css`:

```css
/* ---------- Editor takeover panel ---------- */
/* Same surface as the font browser: it covers the settings column entirely
   rather than sitting inside it, because the column is too narrow to show a
   tab and a panel at once. */
.editor-panel {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  background: var(--panel);
}
.editor-panel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.editor-panel-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
}
.editor-panel-back:hover {
  background: var(--sunken);
}
.editor-panel-title {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}
.editor-panel-title:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
.editor-panel-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
}
```

- [ ] **Step 4: Give the column a positioning context**

`.editor-panel` is absolutely positioned against `.edit-menu`, so that element needs `position: relative`. Find `.edit-menu` in `src/builder/styles/01-base-layout.css` or `02-base-editor.css` (`grep -rn "^\.edit-menu" src/builder/styles/`) and add `position: relative;` to its rule if it is not already there.

- [ ] **Step 5: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add src/builder/components/EditorPanel.tsx src/builder/i18n src/builder/styles
git commit -m "feat(builder): a shared shell for the column's takeover panels"
```

Nothing renders it yet, so there is nothing to see in the browser at this point. That is expected.

---

### Task 3: The card panel, reachable alongside the design tab

Card design moves into a panel opened by clicking the card. The `עיצוב` tab stays for now, rendering the same content, so nothing becomes unreachable mid-plan.

**Files:**
- Create: `src/builder/components/CardPanel.tsx`
- Modify: `src/builder/components/EditingView.tsx`

**Interfaces:**
- Consumes: `EditorPanelShell` (Task 2); `panelReducer`, `INITIAL_PANEL`, `EditorPanel` (Task 1).
- Produces: `<CardPanel popup onChange onClose />`.

- [ ] **Step 1: Write the card panel**

`DesignEditor` is exported from `src/builder/components/SettingsEditor.tsx:1112` and already renders exactly the four groups the card panel needs. Wrap it rather than copying it.

Create `src/builder/components/CardPanel.tsx`:

```tsx
import type { PopupModal } from '@schema';
import { EditorPanelShell } from './EditorPanel';
import { DesignEditor } from './SettingsEditor';
import { useT } from '../i18n';

/**
 * The card's own panel: how the card looks, and what it holds.
 *
 * The design half is `DesignEditor` unchanged, which is what the עיצוב tab
 * used to show. The outline below it is the answer to the one thing pointing
 * at the canvas cannot do: find a field that is scrolled out of a tall card,
 * or faded because it is private. It sits under the design groups because
 * design is what an author opens this panel for.
 */
export function CardPanel({
  popup,
  onChange,
  onSelectField,
  onClose,
}: {
  popup: PopupModal;
  onChange: (patch: Partial<PopupModal>) => void;
  /** Open a field's own panel from the outline. */
  onSelectField: (id: string) => void;
  onClose: () => void;
}) {
  const t = useT();
  return (
    <EditorPanelShell title={t.edit.panelCard} onClose={onClose}>
      <DesignEditor popup={popup} onChange={onChange} />
      {/* The outline is added in Task 7, once the field panel it opens exists. */}
      <FieldOutlinePlaceholder />
    </EditorPanelShell>
  );
}
```

Do **not** write `FieldOutlinePlaceholder`. Delete that line and its comment before finishing this step — the outline lands in Task 7, and a placeholder component committed now is a placeholder that survives. The file should end with `</EditorPanelShell>` after `<DesignEditor .../>`, and `onSelectField` should not be in the props yet either. Add both in Task 7.

So the props for this task are exactly `{ popup, onChange, onClose }`.

- [ ] **Step 2: Wire panel state into EditingView**

In `src/builder/components/EditingView.tsx`:

Add to the imports:

```tsx
import { useReducer } from 'react';
import { panelReducer, INITIAL_PANEL } from '../editorPanel';
import { CardPanel } from './CardPanel';
```

(`useState` is already imported from `react`; add `useReducer` to that existing import rather than a second import line.)

Below `const [tab, setTab] = useState<Tab>('formType');` (line 86), add:

```tsx
  // What has taken the settings column over, if anything. See editorPanel.ts.
  const [panel, dispatchPanel] = useReducer(panelReducer, INITIAL_PANEL);
```

Change `selectItem` (line 110) so a click on empty card space opens the card panel instead of only clearing the selection:

```tsx
  const selectItem = (id: string | null) => {
    dispatchPanel({ type: 'canvasClick', id });
    if (id !== null) setTab('fields');
    setSelectedId(id);
    if (id !== null && stepping) {
      const at = stepIndexOfItem(sortedItems(popup), id);
      if (at >= 0) setStep(at);
    }
  };
```

In the tab panel block (around line 260), render the card panel over the tab content when it is open. Replace:

```tsx
          <div className="tab-panel">
```

with:

```tsx
          <div className="tab-panel">
            {panel.kind === 'card' && (
              <CardPanel
                popup={popup}
                onChange={patch}
                onClose={() => dispatchPanel({ type: 'close' })}
              />
            )}
```

and add `dispatchPanel({ type: 'tabChange' })` to the tab button's `onClick` (line 253), so it becomes:

```tsx
                onClick={() => {
                  dispatchPanel({ type: 'tabChange' });
                  setTab(tabDef.id);
                }}
```

- [ ] **Step 3: Verify by hand in the browser**

```bash
npm run dev
```

Open the printed URL. The dev page shows a diagnostics demo above the builder; hide it and scroll down:

```js
document.getElementById('invalid-field-demo').style.display='none';
document.getElementById('root').scrollIntoView();
```

Click the card's background in the canvas, away from any field. Expect the card panel to cover the settings column, showing the same groups the עיצוב tab shows, with a back chevron. Press Esc, then press back — both should return to the tab. Switch tabs while it is open; it should close.

- [ ] **Step 4: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add src/builder/components/CardPanel.tsx src/builder/components/EditingView.tsx
git commit -m "feat(builder): clicking the card opens its design"
```

---

### Task 4: The field panel, reachable alongside the fields tab

**Files:**
- Create: `src/builder/components/FieldPanel.tsx`
- Modify: `src/builder/components/ContentItemsEditor.tsx`
- Modify: `src/builder/components/EditingView.tsx`

**Interfaces:**
- Consumes: `EditorPanelShell`; `panelFieldId`.
- Produces: `<FieldPanel popup onChange itemId onClose />`.

- [ ] **Step 1: Read what is being moved**

`src/builder/components/ContentItemsEditor.tsx` renders three states, documented in a comment around line 172: the picker, the selected item's settings, and otherwise the outline. The middle one — the `selectedItem ?` branch, roughly lines 186 onward, the block whose root is `<div key="focus" className="layout-list item-focus">` — is the field panel. Read it in full before extracting.

- [ ] **Step 2: Extract it**

Move that branch's JSX into `src/builder/components/FieldPanel.tsx`, wrapped in `EditorPanelShell`. It needs the same values the branch uses today: `items`, `selectedItem`, `selectedGroup` (from `styleGroupFor(selectedItem.type, brand)`), `ruleCtx`, and the update/remove callbacks. Recompute them inside `FieldPanel` from `popup` and `itemId` rather than threading them in — the derivations are three lines each and are already written at the top of `ContentItemsEditor` (lines 86-98).

The panel's title is the item's type name, which `ContentItemsEditor`'s existing header already builds; reuse that expression rather than inventing a new label.

Replace the extracted branch in `ContentItemsEditor` with a render of `<FieldPanel .../>` so the fields tab keeps working identically. The two surfaces render the same component from this point on.

- [ ] **Step 3: Route canvas clicks on a field to the panel**

In `EditingView.tsx`, add to the `tab-panel` block, beside the card panel from Task 3:

```tsx
            {panel.kind === 'field' && (
              <FieldPanel
                popup={popup}
                onChange={patch}
                itemId={panel.id}
                onClose={() => dispatchPanel({ type: 'close' })}
              />
            )}
```

and import `FieldPanel`.

- [ ] **Step 4: Verify by hand**

Click a field in the canvas: its panel takes the column. Click a different field: the panel switches without closing. Press Esc: back to the tab. The fields tab still lists fields and still opens the same panel when a row is clicked.

- [ ] **Step 5: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add src/builder/components/FieldPanel.tsx src/builder/components/ContentItemsEditor.tsx src/builder/components/EditingView.tsx
git commit -m "feat(builder): clicking a field opens its settings and design"
```

---

### Task 5: Add opens the picker in the column

**Files:**
- Modify: `src/builder/components/EditingView.tsx`
- Modify: `src/builder/components/PreviewPane.tsx`
- Modify: `src/builder/components/FieldPicker.tsx`
- Delete: `src/builder/components/AddMenu.tsx`

**Interfaces:**
- Consumes: `panelReducer` action `openPicker`.
- Produces: nothing new.

- [ ] **Step 1: Understand why this matters before changing it**

`FieldPicker.tsx:142` reads `const drag = variant === 'panel' ? insert : null;`. The comment above it explains: the popover hangs over the canvas, so a drag would start on top of its own target. The file's own docblock calls that drag *"the only way to say 'this field goes there', and the only way to put two fields on one row from here."* Moving Add into the column is what keeps that capability once the fields tab is gone.

- [ ] **Step 2: Render the picker as a panel**

In `EditingView.tsx`, add beside the other panels:

```tsx
            {panel.kind === 'picker' && (
              <EditorPanelShell
                title={t.picker.title}
                onClose={() => dispatchPanel({ type: 'close' })}
              >
                <FieldPicker
                  items={sortedItems(popup)}
                  onAddType={addItem}
                  onAddField={addField}
                  onClose={() => dispatchPanel({ type: 'close' })}
                />
              </EditorPanelShell>
            )}
```

Check the actual title key with `grep -n "title" src/builder/i18n/en.ts | sed -n '1,20p'` and use the existing picker heading rather than adding a string. `FieldPicker`'s default `variant` is `'panel'`, so drag is on with no prop needed.

- [ ] **Step 3: Point the toolbar button at it**

`PreviewPane.tsx:232` renders `<AddMenu .../>`. Replace `AddMenu` with a plain toolbar button that calls a new `onOpenPicker` prop, and have `EditingView` pass `() => dispatchPanel({ type: 'openPicker' })`. Model the button on the `toolbar-btn` beside it (the mode toggle at `PreviewPane.tsx:236`), reusing `AddMenu`'s existing label and icon so the bar looks unchanged.

- [ ] **Step 4: Delete the dead code**

```bash
rm src/builder/components/AddMenu.tsx
```

In `FieldPicker.tsx`, remove the `variant` prop, the `in-popover` class it toggles, the `variant === 'popover'` branch in the header (around line 183), and simplify `const drag = variant === 'panel' ? insert : null;` to `const drag = insert;`. Then confirm nothing still refers to the removed things:

```bash
grep -rn "AddMenu\|in-popover\|variant" src/builder/components/FieldPicker.tsx src/builder --include=*.tsx | grep -i "popover\|addmenu"
```

Expected: no hits outside `PublishPopover`, which is unrelated.

- [ ] **Step 5: Verify by hand**

Press `+ הוספה`. The picker fills the settings column. Drag a row onto the canvas and drop it beside an existing field: it lands there, on the same row. Click a row instead: it appends. Both should work.

- [ ] **Step 6: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add -A
git commit -m "feat(builder): Add opens the picker in the column, where rows can be dragged"
```

---

### Task 6: Split SettingsEditor and drop to three tabs

Every panel is now reachable from the canvas, so the two tabs can go.

**Files:**
- Create: `src/builder/components/FormTypeEditor.tsx`
- Modify: `src/builder/components/SettingsEditor.tsx`
- Modify: `src/builder/components/EditingView.tsx`
- Modify: `src/builder/i18n/en.ts`, `src/builder/i18n/he.ts`

- [ ] **Step 1: Split the component**

`SettingsEditor` (line 99) renders, in order: the form-name row, the Delivery `Accordion`, `<StepsSection />` (line 250), and the Submission `Accordion` (line 252 to the end of its return).

Move the form-name row and the Delivery accordion into `src/builder/components/FormTypeEditor.tsx` as `export function FormTypeEditor({ popup, onChange }: Props)`. Take with them only the helpers they use: `inline`, `setTrigger`, `setPlacement`, and the `TRIGGER_TYPES` / `PLACEMENTS` constants they read.

Leave `StepsSection` and the Submission accordion in `SettingsEditor`, which keeps its name and its `showEndpoint` prop and is now the `הגדרות` tab.

- [ ] **Step 2: Rename the tab strings**

In `src/builder/i18n/en.ts` (lines 34-37): delete `tabDesign`, and change `tabFields: 'Fields'` to `tabSettings: 'Settings'`.

In `src/builder/i18n/he.ts` (lines 31-34): delete `tabDesign`, and change `tabFields: "שדות"` to `tabSettings: "הגדרות"`.

- [ ] **Step 3: Cut the tab list to three**

In `EditingView.tsx`, replace the `Tab` type (line 41) and `TABS` (lines 49-54):

```tsx
type Tab = 'formType' | 'settings' | 'automations';

// Text-only tabs: the row reads as an underlined rail, so a glyph per tab would
// only add noise. `labelKey` resolves against `t.edit` at render so tabs stay
// translated.
//
// Three, not four. Design left the rail because it is reached by pointing at
// the thing being designed, and the field list left with it: both are panels
// the canvas opens. What is left is configuration, in the order it is decided:
// what kind of form this is, how it is wired up, and what happens after.
const TABS: { id: Tab; labelKey: keyof Strings['edit'] }[] = [
  { id: 'formType', labelKey: 'tabFormType' },
  { id: 'settings', labelKey: 'tabSettings' },
  { id: 'automations', labelKey: 'tabAutomations' },
];
```

Replace the four render lines (261-274) with:

```tsx
            {tab === 'formType' && <FormTypeEditor popup={popup} onChange={patch} />}
            {tab === 'settings' && <SettingsEditor popup={popup} onChange={patch} showEndpoint={showEndpoint} />}
            {tab === 'automations' && <AutomationsSection popup={popup} onChange={patch} />}
```

Remove the `design` and `fields` cases, the `ContentItemsEditor` import and its render, and the `DesignEditor` import (the card panel imports it now, not this file).

In `selectItem`, drop `if (id !== null) setTab('fields');` — there is no fields tab to switch to, and `dispatchPanel` already opens the panel.

- [ ] **Step 4: Verify by hand**

Three tabs. `סוג טופס` shows the form name and Delivery. `הגדרות` shows Steps and Submission. `אוטומציות` is unchanged. Clicking a field or the card still opens its panel from any tab.

- [ ] **Step 5: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add -A
git commit -m "feat(builder): three tabs, split along what is configured and what is designed"
```

---

### Task 7: The field outline, in the card panel

`ContentItemsEditor` is now unused as a tab, and its third state — the outline of every item — is the piece worth keeping.

**Files:**
- Modify: `src/builder/components/CardPanel.tsx`
- Modify or delete: `src/builder/components/ContentItemsEditor.tsx`
- Modify: `src/builder/components/EditingView.tsx`

- [ ] **Step 1: Reduce ContentItemsEditor to the outline**

Its picker state moved to Task 5 and its selected-item state to Task 4, so only the list remains. Strip the other two branches and the `picking` state, and rename the export to `FieldOutline`. It keeps the list's own affordances — reorder and remove — since those are what make it an alternative to pointing rather than a picture of the form. Its `onSelect` now opens the field panel.

If what remains is small enough, move it into `CardPanel.tsx` and delete the file; if it is still substantial, keep it as `src/builder/components/FieldOutline.tsx`. Judge by whether the outline reads clearly beside the card panel's own body.

- [ ] **Step 2: Render it under card design**

In `CardPanel.tsx`, add the `onSelectField` prop promised in Task 3 and render the outline beneath `<DesignEditor />` inside its own `Accordion` (imported from `./Accordion`), titled `t.edit.panelFields`, so the panel opens on design and the outline is one click away rather than competing for the top:

```tsx
      <DesignEditor popup={popup} onChange={onChange} />
      <Accordion title={t.edit.panelFields}>
        <FieldOutline popup={popup} onChange={onChange} onSelect={onSelectField} />
      </Accordion>
```

- [ ] **Step 3: Pass the handler**

In `EditingView.tsx`, give `CardPanel` its new prop:

```tsx
                onSelectField={(id) => dispatchPanel({ type: 'canvasClick', id })}
```

- [ ] **Step 4: Verify by hand**

Click the card. Below the design groups, a collapsible group lists every item, private ones included. Clicking one opens its field panel. Reordering from the list still moves it in the canvas.

- [ ] **Step 5: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add -A
git commit -m "feat(builder): the field outline lives under card design"
```

---

### Task 8: Step switching on the canvas

Step *configuration* is in `הגדרות`, but *switching* which step the canvas shows cannot be: an author editing step 2's fields must not be sent to another tab to get there.

**Files:**
- Modify: `src/builder/components/PreviewPane.tsx`
- Modify: `src/builder/components/EditingView.tsx`

- [ ] **Step 1: Move the switcher**

`EditingView` already owns `activeStep`, `setStep`, `addNewStep` and `dropStep`, and already passes `activeStep` and `onStepChange` into `ContentItemsEditor`. Pass them to `PreviewPane` instead and render the switcher in the toolbar's first `toolbar-group`, beside the Add button, separated by the existing `<span className="toolbar-sep" aria-hidden="true" />`.

Reuse the step rail markup from whatever `ContentItemsEditor` rendered for it rather than designing a new control.

- [ ] **Step 2: Show it only when there is a choice**

Guard on `stepsEnabled(popup) && stepCount(popup.contentItems) > 1`. A one-step form has nothing to switch between and the control would be noise.

- [ ] **Step 3: Verify by hand**

Turn steps on in `הגדרות` and add a step. The switcher appears in the canvas toolbar; changing it changes what the canvas shows. Turn steps off: it disappears.

- [ ] **Step 4: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add -A
git commit -m "feat(builder): switch steps from the canvas, where the fields are"
```

---

### Task 9: Closing, and reaching the card by keyboard

**Files:**
- Modify: `src/builder/components/PreviewPane.tsx`
- Modify: `src/builder/components/EditingView.tsx`

- [ ] **Step 1: Close on a click outside the card**

The renderer's own handler sits on `.pm-body` and never fires for the canvas area around the card, so this is a new handler in the builder. Add an `onClick` to the canvas wrapper in `PreviewPane` that dispatches `close` when the click's `composedPath()` does not include the card element. `composedPath()` rather than `e.target`, because the builder is in a shadow root and targets are retargeted at the host — `Select.tsx` and `SettingsEditor`'s `useDismissable` both do this already; copy their approach.

- [ ] **Step 2: Give the card a keyboard route**

Card design used to be reachable through a tab and now is not, so removing the tab must not remove the capability. The card needs a focusable, named affordance that dispatches `{ type: 'canvasClick', id: null }` on Enter or Space.

Prefer adding it in the builder's canvas wrapper over changing the renderer: the renderer ships to visitors, and a control that only the editor needs does not belong in it. If that proves impossible, a builder-only affordance gated on the existing `editable` flag is acceptable — but check first.

- [ ] **Step 3: Verify by hand**

Tab through the editor: the card is reachable and announces itself, and Enter opens its panel. Click the checkered area outside the card: any open panel closes. Click a field: its panel opens rather than closing.

- [ ] **Step 4: Verify and commit**

```bash
npm run typecheck && npm test 2>&1 | grep -E "^ℹ (tests|pass|fail)"
git add -A
git commit -m "feat(builder): close a panel by clicking away, and reach the card by keyboard"
```

---

### Task 10: Whole-flow check and PR

- [ ] **Step 1: Full verification**

```bash
npm run typecheck
npm test
npm run build:all
```

All three must pass. `npm test` should report `fail 0` with at least 143 tests (132 before this plan, plus Task 1's 11).

- [ ] **Step 2: Walk the editor end to end**

With `npm run dev`, in one sitting: create a form, add a field by clicking a picker row, add another by dragging a row onto the canvas beside the first, style one of them from its panel, restyle the card from the card panel, find a field through the outline, turn on steps and switch between them from the canvas, add an automation, and publish. Nothing should be unreachable and nothing should need a tab that no longer exists.

- [ ] **Step 3: Confirm the JSON did not move**

The global constraint is that a host's JSON is unchanged. Press "הצג JSON" in the dev page before and after a no-op edit and confirm the shape matches what `main` produces for the same form.

- [ ] **Step 4: Open the PR**

```bash
git push -u origin feat/editor-tabs-restructure
gh pr create --base main --title "feat(builder): three tabs, and design where the design is"
```

Note in the PR body that this branch was cut from `fix/default-layout-image` (PR #16) and should merge after it.

---

## Self-review notes

- **Spec coverage.** Three tabs (Task 6), field panel (4), card panel (3), outline (7), Add as panel (5), step switcher (8), navigation and a11y (9). The spec's URL line under `סוג טופס` is corrected above rather than implemented, because URLs are not in a tab.
- **Tasks 4, 7, 8 and 9 describe moves rather than quoting code.** That is deliberate: they relocate blocks of 40 to 200 lines that already exist and are already commented, and transcribing them into this plan would produce a copy that drifts from the original before anyone reads it. Each names the exact file, the exact block, and how to find it. Read the source, then move it.
- **Ordering is load-bearing.** Panels become reachable (3, 4, 5) before tabs are removed (6). Do not reorder those.
