# DiagramExportMenu

Taking the figure off the page: PNG, JPEG, WebP, SVG and a 1200×630 share card.

- Group: Diagrams
- Import: `import { DiagramExportMenu } from '@misoto22/folio/diagrams'`
- Page: https://ui.misoto22.com/components/diagram-export-menu/
- Related: dropdown-menu, diagram-toolbar

## When to reach for it

It does the export rather than emitting a format name, because the interesting half — baking custom properties into real colours before serialising — is the half a caller would not know to write.

## Anatomy

- **Trigger** (required) — A small secondary button reading Export, or whatever trigger replaces it — the replacement is slotted, so it becomes the menu’s own control rather than sitting next to one.
- **Menu** (required) — A dropdown aligned to the trigger’s end, in three labelled groups separated by rules: Image, Vector, Share. The grouping is the answer to "which of these five do I want".
- **Format row** (required) — One menu row per format, with its name and a mono hint under it — "Lossless, 2× for retina", "Compact, flattened onto paper". Real menu rows, so the arrow keys walk them, typing jumps to one, and picking one closes the menu over the file it just wrote. The running one gains an ellipsis and every row is disabled until it finishes, so a second click cannot start a second export.
- **Serialiser** (required) — The part with nothing to point at: the artwork is cloned, every node’s computed paint is written inline, and the clone is placed on a plate the size of the picture plus its padding. It is what stops var(--ink) arriving in a document with no stylesheet and painting nothing.
- **Share card** — A fixed 1200 × 630 frame with the title printed on it and the whole diagram letterboxed inside — never cropped to fill, because a card cropped to fill is a picture of a different diagram.
- **Result** — onResult, called with the format, an ok or the Error, and the source that produced it. It is the only place a failed export is reported.

## Best practices

### Do

- Point targetRef at the wrapper rather than at an svg you found yourself. The export looks for the artwork marker first, which is what stops a page whose toolbar sits inside the same wrapper from exporting a picture of a chevron.
- Export after the artwork has been measured. The serialiser reads the element’s box and throws when it is still zero, so a call made in the same tick as the mount fails loudly instead of writing an empty file.
- Take onResult and put a failure in front of the reader. An unmeasured figure, a canvas tainted by a cross-origin image, a browser that returned no 2D context — each is reported there rather than thrown at the click, and the alternative is a menu item that quietly does nothing.
- Read source alongside ok. built-in says a file reached the browser; caller says your own onExport resolved, which is exactly what a handler that did nothing also does — the menu cannot see inside your pipeline and no longer reports as though it can.
- Pass background={null} for a figure going onto a coloured page. Every other export paints the reader’s own surface behind the artwork, and JPEG is flattened onto that surface whatever this says, because a transparent JPEG is a black one.

### Don’t

- The file is the artwork and the theme the reader was in. Paint is read off the live element, so a figure exported from a dark page is a dark image in a light document; and the toolbar, the inspector and the figure’s own hidden summary list are HTML, so none of them travels with it.
- Do not treat the SVG as a pixel-exact record. The isolated document cannot fetch the page’s web fonts, so the type falls back to what the machine has — the words and the line breaks are already fixed, but a name that just fitted its plate on screen can overrun its own rule in the file.
- The output is sized from the artwork’s box ON SCREEN rather than from its viewBox, so a figure sitting at 4× inside a DiagramCanvas serialises four times larger and then rasterises at 2× on top of that. Reset the view before exporting, or a reader gets a file whose dimensions record where the zoom happened to be.

## Accessibility

- A failed export is reported through onResult rather than swallowed: a click that quietly does nothing is indistinguishable from a broken button.

## DiagramExportMenu

Taking the figure off the page: five files, one menu. WHY THE MENU DOES THE WORK rather than handing back a format: every one of these exports is the same six steps — find the `<svg>`, walk it with `getComputedStyle` to bake the custom properties into real colours, serialise, rasterise, name the file, hand it to the browser — and only the last two differ between them. A menu that emitted `'png'` and left the caller to do the rest would be a menu that every consumer reimplements, badly, and the interesting half (a serialised SVG resolves `var(--ink)` to nothing and comes out invisible) is exactly the half a caller would not know to write. `onExport` is still there for a page with its own pipeline — a server-side renderer, a different frame size — and taking it turns everything below into a no-op, which is why a result from that path says `source: 'caller'`: this component cannot see whether the handler produced a file, so it does not report that it did. What each format actually is, stated rather than implied. **SVG** is the artwork with resolved colours. It is editable and it is the only lossless one, but it carries no web fonts: a machine without the family renders it in a fallback, so type metrics will differ. **The ground** is the reader's own surface unless `background` says otherwise. `background={null}` exports on transparency — for a figure being dropped onto a coloured page — except in JPEG, which has no alpha and is flattened onto paper whatever is asked for. **PNG, JPEG and WebP** are the browser's own rasteriser re-drawing that SVG at 2×. Not a screenshot — antialiasing and any effect a page stylesheet applied from OUTSIDE the `<svg>` are not in it. **JPEG has no alpha**, so it is flattened onto the resolved paper colour rather than onto black, which is what a transparent PNG becomes when a format with no transparency is asked to hold it. **The share card** is a 1200 × 630 frame with the title on it and the whole diagram letterboxed inside — never cropped. A card that cropped to fill the frame would be a picture of a different diagram.

### Props

- `targetRef` (required) — `RefObject<HTMLElement | SVGSVGElement | null>`. The `<svg>` to export, or an element containing exactly one. A ref rather than a selector, because a page can hold several figures and a selector would export whichever the document happened to reach first.
- `title` (required) — `string`. Names the file, and is printed on the share card.
- `trigger` — `ReactNode`. Replaces the trigger.
- `className` — `string`.
- `onExport` — `(format: ExportFormat) => void | Promise<void>`. Runs instead of the built-in export — for a caller with its own pipeline. Taking it turns everything below into a no-op, INCLUDING the part that knows whether a file was produced: a result from this path reports `source: 'caller'` and an `ok` that means only "your handler resolved".
- `onResult` — `(result: ExportResult) => void`. Told what happened, so a page can raise a toast.
- `background` — `string | null`. Painted behind the artwork. Defaults to the surface the reader is looking at; `null` exports on a transparent ground. JPEG has no alpha channel, so it is flattened onto the reader's own paper whatever this says — a transparent JPEG is a black one.

## exportFigure

One figure, one format, one file. Exported so a page can wire a keyboard shortcut or its own button to exactly what the menu does, without reimplementing the six steps.

## Example — default

```tsx
import { ArchitectureFigure, DiagramExportMenu } from '@misoto22/folio/diagrams'

<div className="flex flex-col items-start gap-4">
  <DiagramExportMenu targetRef={figure} title="Request path" />
  <div ref={figure} className="w-full">
    <ArchitectureFigure spec={SPEC} heading={false} legend="hidden" />
  </div>
</div>
```

## Example — what lands in the file

```tsx
import { Button } from '@misoto22/folio'

<div ref={stage} className="flex flex-col gap-4">
  <DiagramToolbar label="Diagram actions">
    <DiagramToolbarGroup>
      <Button size="sm" variant="ghost" iconOnly aria-label="About this diagram">
        <RiInformationLine size={14} aria-hidden />
      </Button>
    </DiagramToolbarGroup>
    <DiagramToolbarGroup>
      <DiagramExportMenu targetRef={stage} title="Order pipeline" />
    </DiagramToolbarGroup>
  </DiagramToolbar>
  <ArchitectureFigure spec={SPEC} heading={false} legend="hidden" />
</div>
```

## Example — your own pipeline

```tsx
import { Button } from '@misoto22/folio'
import { ArchitectureFigure, DiagramExportMenu } from '@misoto22/folio/diagrams'

<div className="flex flex-col items-start gap-4">
  <div className="flex flex-wrap items-center gap-3">
    <DiagramExportMenu
      targetRef={figure}
      title="Request path"
      trigger={
        <Button size="sm" variant="ghost" iconOnly aria-label="Export this diagram">
          <RiDownloadLine size={14} aria-hidden />
        </Button>
      }
      onExport={(format) => setNote(`Queued a ${format} render on the server.`)}
      onResult={(result) => {
        if (!result.ok) setNote(`The ${result.format} export failed: ${result.error?.message}`)
      }}
    />
    <span className="mono-meta text-(--ink-2)">{note}</span>
  </div>
  <div ref={figure} className="w-full">
    <ArchitectureFigure spec={SPEC} heading={false} legend="hidden" />
  </div>
</div>
```
