# Driving the web Match Builder

The Stage Setter editor (`https://itsmatchday.app/build/`) ships with a complete Match Builder dialog. The skill drives it via `mcp__Claude_Preview__preview_eval` to inject a composed match, render it visually, and trigger the .usmatch download.

## Setup

Add a preview entry named `match-day-editor` in your `.claude/launch.json` pointing at the hosted editor:

```json
{
  "version": "0.0.1",
  "configurations": [
    { "name": "match-day-editor", "url": "https://itsmatchday.app/build/" }
  ]
}
```

Then in the skill:

```python
mcp__Claude_Preview__preview_start(name="match-day-editor")
# (note the serverId; use it for all subsequent preview_eval calls)
```

## The in-memory matchBuilder state

The editor exposes a global `matchBuilder` object whose fields the skill mutates directly:

```javascript
const matchBuilder = {
  id: null,                 // UUID string; auto-minted on first open if null
  name: '',                 // match name (required, non-empty at export)
  date: '2026-05-30',       // YYYY-MM-DD (date only) — gets ISO-ified at export
  notes: '',                // optional free-text
  matchDirector: { name: '', uspsa: '', email: '' },
  venue: { name: '', address: '', rules: '', parking: '', restrooms: '', contact: '' },
  stages: [
    { kind: 'ref',    ref:   'CM 99-08' },
    { kind: 'inline', stage: { /* full Stage v2 JSON */ } },
    // …
  ],
};
```

**Important:** the in-memory `stages[]` uses a `kind: 'ref' | 'inline'` discriminator and stores the payload under `ref` or `stage` respectively. The on-disk `.usmatch` format drops the `kind` field and uses `{ ref: 'CM 99-08' }` / `{ inline: { ... } }` directly. The editor's `mbBuildUsmatchPayload()` does the translation.

## Injecting a composed match

Once you've composed the match JSON in the skill, inject it into the editor like this:

```javascript
// preview_eval payload:
(() => {
  // Reset state to your composed match. Mutate fields one by one so
  // any non-enumerable bindings stay intact.
  matchBuilder.id = "<your-uuid>";
  matchBuilder.name = "APSC Saturday Pistol";
  matchBuilder.date = "2026-05-30";
  matchBuilder.notes = "9 AM safety brief. Cold range.";
  matchBuilder.matchDirector.name = "Jory Hanus";
  matchBuilder.matchDirector.uspsa = "A12345";
  matchBuilder.matchDirector.email = "jory@example.com";
  matchBuilder.venue.name = "Brazos Valley Shooting Club";
  matchBuilder.venue.address = "Bryan, TX";
  matchBuilder.venue.rules = "USPSA handgun. Major PF only.";
  matchBuilder.venue.parking = "Gravel lot, east side.";
  matchBuilder.venue.restrooms = "Portable at clubhouse.";
  matchBuilder.venue.contact = "MD: Jory, (979) 555-1234";
  // Replace the stages array entirely
  matchBuilder.stages.length = 0;
  matchBuilder.stages.push({ kind: 'ref', ref: 'CM 99-08' });
  matchBuilder.stages.push({ kind: 'ref', ref: 'CM 99-28' });
  matchBuilder.stages.push({ kind: 'inline', stage: /* paste Stage JSON */ });
  // Persist to localStorage so opening the dialog later sees the same state.
  mbPersistDraft();
  // Open the dialog
  openMatchBuilder();
  return 'ok';
})();
```

If you're injecting a large `inline` stage, pass it via a `const stageJson = ...` first, then push `{ kind: 'inline', stage: stageJson }`. Don't try to inline kilobytes of JSON inside the `push()` call — it gets unreadable in screenshots.

## Refreshing the UI after a state change

After mutating `matchBuilder`, call the renderers to make the change visible without reopening the dialog:

```javascript
mbSyncInputs();        // syncs name/date/notes inputs from state
mbRenderStageList();   // re-renders the stage rows in the sidebar
mbRenderDetailPane();  // re-renders the selected stage's detail pane
mbRenderHeaderCards();  // re-renders the basics/director/venue cards
```

## Producing the .usmatch payload

Two paths depending on what the skill needs:

### Get the JSON without downloading (for skill-side file writing)

```javascript
const { json, safeName, filename } = mbBuildUsmatchPayload();
// json = stringified payload
// safeName = filename-safe match name
// filename = "<safeName>.usmatch"
```

Then the skill writes `json` to disk via the `Write` tool. This is the **preferred path** — gives the skill control over where the file lands and avoids triggering a browser download in a headless preview environment.

### Trigger the browser's download dialog

```javascript
mbGenerate();  // = mbDispatch('download') → validates + calls mbWriteUsmatch()
```

`mbGenerate()` runs validation (name non-empty, ≥1 stage, no placeholders) and routes through the credit-review dialog if inline stages need attribution confirmation. The download appears in the user's default Downloads folder.

## Validation gates

`mbDispatch('download')` enforces (and bounces back to the user if violated):
1. `matchBuilder.name.trim().length > 0`
2. `matchBuilder.stages.length >= 1`
3. No `kind: 'inline'` stages with a placeholder flag
4. If any inline stages exist, the credit-review dialog must be confirmed before download proceeds

Pre-validate from the skill side before injection so the user doesn't see the validation popup mid-flow.

## Reading current state back

To screenshot the result and verify the injection worked:

```javascript
JSON.stringify(matchBuilder);
```

Or grab the actual export payload:

```javascript
JSON.stringify(mbBuildUsmatchPayload());
```

Both return strings small enough to fit in the eval result.

## Switching between editor and wizard mode

```javascript
mbSetMode('wizard');  // OR 'editor'
```

The dialog flips between the sidebar+detail-pane layout (editor) and the 5-step linear wizard. Editor mode is faster for an agent to drive (one dialog, all fields visible); wizard mode is friendlier for the user to review.

Default is whatever the user last used (persisted to localStorage as `stagesetter-match-builder-mode`).

## Docking the dialog as a right-side panel

```javascript
mbToggleDock();  // toggles dock state, persists to localStorage
```

When docked, the dialog occupies a ~380px right-side panel and the editor canvas remains interactive. Useful when the user wants to author a custom stage in the editor and add it to the match without closing/reopening the dialog.

## Closing the dialog

```javascript
document.getElementById('match-builder-dialog').close();
```

Closing doesn't clear state — the next `openMatchBuilder()` restores from localStorage. To explicitly clear: `mbClearDraft()`.

## Recommended skill drive sequence

```
1. preview_start(name="match-day-editor")
2. (wait for page load — preview_eval to confirm `typeof matchBuilder !== 'undefined'`)
3. preview_eval — inject the composed match (see "Injecting a composed match" above)
4. preview_screenshot — show the user the rendered match builder
5. If the user wants to iterate: mutate matchBuilder fields + call mbRender*() functions, screenshot again
6. preview_eval — `JSON.stringify(mbBuildUsmatchPayload())` to get the final payload
7. Write the JSON to disk via the Write tool, with `.usmatch` extension
```

Skipping the editor entirely (no preview, no screenshots) is also valid for users who just want the file:

```
1. Compose the Match JSON in the skill from the interview answers + classifier index
2. Validate (name + ≥1 stage + no inline-placeholder)
3. Write the JSON to disk via the Write tool, with `.usmatch` extension
4. Tell the user the file path and how to open it in Match Day
```

The skill should default to the editor-driven path because seeing the result on-screen catches mistakes earlier — but offer the headless path if the user says "just give me the file."
