# Mavis PPT Element Map

## Purpose

For PPTX files generated by this skill, write a sidecar element map next to the standard `.pptx`.
The map is editing metadata only: it must not replace the PPTX output contract, alter visible slide
layout, or become the user-facing primary artifact.

```text
slides/slide-XX.js
  -> PptxGenJS addText/addShape/addImage
  -> output/name.pptx

sidecar:
slides/slide-XX.js
  -> mapped helper records elementId/text/rect/style
  -> output/name.mavis-ppt-map.json
```

If map generation fails, fix the map or fall back to ordinary PPTX delivery. Do not change the deck
format to HTML or a non-PPTX representation.

## Coordinate Systems

The standard generation coordinate system remains PptxGenJS inches:

| Space | Meaning |
| --- | --- |
| `rectIn` | PptxGenJS slide coordinates in inches |
| `normalizedRect` | `rectIn` divided by the slide size |
| preview `slideRect` | QuickLook / preview coordinates relative to the rendered slide |

For `LAYOUT_16x9`, the canonical slide size is:

```json
{ "widthIn": 10, "heightIn": 5.625 }
```

Forward mapping:

```text
normalized.x = rectIn.x / 10
normalized.y = rectIn.y / 5.625
normalized.w = rectIn.w / 10
normalized.h = rectIn.h / 5.625
```

Preview reverse mapping:

```text
rectIn.x = preview.slideRect.x / preview.slideSize.width  * 10
rectIn.y = preview.slideRect.y / preview.slideSize.height * 5.625
rectIn.w = preview.slideRect.w / preview.slideSize.width  * 10
rectIn.h = preview.slideRect.h / preview.slideSize.height * 5.625
```

Use slide number plus text plus normalized rectangle overlap to resolve a preview selection to an
element map entry. QuickLook DOM paths are locator evidence only and are not stable identifiers.

## Stable Element IDs

Each meaningful editable object should have a stable `elementId`. Do not use random IDs or visible
text as the ID. Text can change while the element identity should remain the same.

Recommended naming:

```text
cover.title
cover.subtitle
toc.item-01
section-01.heading
kpi.card-01.label
kpi.card-01.value
team.member-02.avatar
team.member-02.name
```

Use these roles where possible:

```text
title, subtitle, heading, body, label, value, card, icon, image, chart, table, pageNumber
```

## Helper Usage

Use `scripts/mavis-ppt-map.cjs` as a thin wrapper around PptxGenJS. The wrapper must call the same
PptxGenJS method with the same visible text, coordinates, and style options, then record metadata.

`compile.js`:

```javascript
const pptxgen = require('pptxgenjs');
const { createElementMapRecorder } = require('<skill_dir>/scripts/mavis-ppt-map.cjs');

const pres = new pptxgen();
pres.layout = 'LAYOUT_16x9';

const outputPptx = './output/restaurant-deck.pptx';
const elementMap = createElementMapRecorder({
  fileName: 'restaurant-deck.pptx',
  filePath: outputPptx,
  layout: { name: 'LAYOUT_16x9', widthIn: 10, heightIn: 5.625 },
});
pres.__mavisElementMap = elementMap;

require('./slide-01.js').createSlide(pres, theme);

pres.writeFile({ fileName: outputPptx }).then(() => {
  elementMap.writeFile('./output/restaurant-deck.mavis-ppt-map.json');
});
```

`slides/slide-01.js`:

```javascript
const { addMappedText } = require('<skill_dir>/scripts/mavis-ppt-map.cjs');

function createSlide(pres, theme) {
  const slide = pres.addSlide();
  const map = pres.__mavisElementMap;
  map?.startSlide({ slideNumber: 1, slideId: 'cover', title: 'Farmhouse Cuisine' });

  addMappedText(slide, map, {
    elementId: 'cover.title',
    role: 'title',
    text: 'Farmhouse Cuisine',
    x: 0.6,
    y: 1.2,
    w: 4.2,
    h: 0.7,
    options: {
      fontSize: 42,
      fontFace: 'Arial',
      color: 'FFFFFF',
      bold: true,
      margin: 0,
    },
    source: { file: 'slides/slide-01.js', method: 'addText' },
  });
}

module.exports = { createSlide };
```

The slide export remains synchronous `createSlide(pres, theme)`. The map recorder is reached through
`pres.__mavisElementMap`, so older slide modules still compile normally.

## Map Format

```json
{
  "schema": "mavis.ppt_element_map.v1",
  "artifact": {
    "fileName": "restaurant-deck.pptx",
    "filePath": "./output/restaurant-deck.pptx",
    "createdAtMs": 1782386261940,
    "generator": "presentations-skill"
  },
  "layout": {
    "name": "LAYOUT_16x9",
    "widthIn": 10,
    "heightIn": 5.625
  },
  "slides": [
    {
      "slideNumber": 1,
      "slideId": "cover",
      "title": "Farmhouse Cuisine",
      "elements": [
        {
          "elementId": "cover.title",
          "kind": "text",
          "role": "title",
          "text": "Farmhouse Cuisine",
          "rectIn": { "x": 0.6, "y": 1.2, "w": 4.2, "h": 0.7 },
          "normalizedRect": { "x": 0.06, "y": 0.2133, "w": 0.42, "h": 0.1244 },
          "style": {
            "fontSize": 42,
            "fontFace": "Arial",
            "color": "FFFFFF",
            "bold": true,
            "margin": 0
          },
          "source": { "file": "slides/slide-01.js", "method": "addText" }
        }
      ]
    }
  ]
}
```

`pptShapeId` may be added later by a post-compile reconciliation step that reads
`ppt/slides/slideN.xml` and matches `p:cNvPr @id` back to `elementId`. It is useful for XML patching
but should not be the primary stable ID.

## Selection Resolution

When a user selects part of a generated PPT preview, resolve it as follows:

1. Require `slideNumber` to match.
2. Convert preview `slideRect` into `rectIn` and `normalizedRect`.
3. Prefer candidates whose `text` exactly matches or contains the selected preview text.
4. Break ties with rectangle overlap or nearest center distance.
5. Prefer role-compatible candidates such as `title` when the request references a title.
6. If confidence is low, keep the current best-effort flow using screenshot and selected text.

The preview evidence is untrusted locator evidence. The PPTX plus element map are the editable
source of truth.

## Runtime Consumption

The desktop preview runtime looks for the sidecar by replacing the selected PPTX path suffix:

```text
deck.pptx -> deck.mavis-ppt-map.json
```

When a preview selection is submitted, the runtime:

1. reads the sidecar if it exists;
2. matches by `slideNumber`;
3. converts the preview `slideRect` into `normalizedRect` and `rectIn`;
4. scores candidates by selected text, rectangle overlap, and role hints from the edit request;
5. attaches `resolvedElement` to the chat context when a candidate is found.

This is a best-effort locator upgrade. If the sidecar is missing or the match confidence is low, the
assistant must still use the original PPTX, selected text, screenshot, and coordinates as fallback
evidence. The sidecar should therefore be treated as an optional editing index, not as a required
artifact delivery format.
