# @magicpatterns/html-to-figma

Converts a live DOM subtree into the binary clipboard payload Figma reads on
paste. Runs in the browser — you hand it the element you want exported, it
returns a `text/html` string you can write to the system clipboard, and
pasting into Figma materialises the tree as real Figma nodes (frames,
text, vectors, images).

## How it works

Two-step flow:

1. **`inlineComputedStyles(root)`** — must run in a browser with the
   element mounted. Walks the subtree, reads computed styles from the
   live layout, stamps each element's inline `style` attribute, and
   records its rendered rect as `data-rect="x,y,w,h"`. Returns the
   serialised HTML string. This snapshot is what the next step consumes
   — it doesn't need the live layout engine anymore.

2. **`generateFromElement(cloneRoot, { topLayerName })`** — parses the
   inlined clone, preloads referenced fonts and images in parallel,
   builds a Figma node tree, encodes the `fig-kiwi` binary envelope, and
   returns the `text/html` string. Write it to the clipboard under the
   `text/html` MIME:

   ```ts
   const html = await generateFromElement(root, { topLayerName: 'MyExport' })
   await navigator.clipboard.write([
     new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }) }),
   ])
   ```

Pressing ⌘V in Figma produces the node tree.

## Install & use

```bash
yarn add @magicpatterns/html-to-figma
```

### New API (preferred)

```ts
import {
  generateFromElement,
  inlineComputedStyles,
} from '@magicpatterns/html-to-figma'

const root = document.getElementById('to-export')!
// Snapshot computed styles + rects from the live DOM.
const inlined = inlineComputedStyles(root)
// Reparse so the generator works on a clean, detached DOM.
const clone = new DOMParser()
  .parseFromString(inlined, 'text/html')
  .body.firstElementChild!
const payload = await generateFromElement(clone, {
  topLayerName: 'MyExport',
})
await navigator.clipboard.write([
  new ClipboardItem({ 'text/html': new Blob([payload], { type: 'text/html' }) }),
])
```

### Exporting multiple screens at once

When a canvas-style UI wants to paste several designs into Figma with
their relative positions preserved, call `generateFromElements` instead
of combining payloads by hand. It composes **one** clipboard archive
with a single wrapper FRAME containing each screen at its given `{x,
y}`. Fonts, images, and vector blobs are de-duplicated across screens.

```ts
import {
  generateFromElements,
  inlineComputedStyles,
} from '@magicpatterns/html-to-figma'

// Ask each iframe / node for its inlined HTML, parse each back to an
// Element, then collect into a ScreenSpec[].
const screens = selectedNodes.map((node) => {
  const inlined = inlineComputedStyles(node.exportRoot)
  const root = new DOMParser()
    .parseFromString(inlined, 'text/html')
    .body.firstElementChild!
  return { root, x: node.x - minX, y: node.y - minY, name: node.name }
})

const payload = await generateFromElements(screens, { name: 'My Canvas' })
await navigator.clipboard.write([
  new ClipboardItem({ 'text/html': new Blob([payload], { type: 'text/html' }) }),
])
```

### Legacy API (for migration)

The original `htmlToFigma` export is still available and returns the
same intermediate JSON tree it always has. It's maintained for
backwards-compatibility while callers migrate to `generateFromElement`.
New code should use the new API.

```ts
import { htmlToFigma } from '@magicpatterns/html-to-figma'

const tree = htmlToFigma(document.getElementById('to-export')!)
// ... existing consumer code handles `tree`
```

## Public API surface

| Export | Kind | Purpose |
|---|---|---|
| `generateFromElement` | async fn | **Preferred.** Returns the Figma clipboard `text/html` payload for one DOM subtree. |
| `generateFromElements` | async fn | Multi-screen variant: composes N subtrees into one payload with each at its given `{x, y}`. |
| `inlineComputedStyles` | fn | Snapshot live-DOM computed styles + rects into an HTML string. |
| `GenerateOptions` | type | Options shape for `generateFromElement`. |
| `GenerateManyOptions` | type | Options shape for `generateFromElements`. |
| `ScreenSpec` | type | Shape of each screen passed to `generateFromElements`. |
| `htmlToFigma` | fn | **Legacy.** Synchronous intermediate-JSON export. |

## Development

### Running the playground

The repo ships a Vite app at `src/dev/` with example components and a
"Copy for Figma paste" button. Use it to try out changes end-to-end
before shipping — `⌘V` into Figma after copying.

```bash
yarn dev           # starts Vite on http://localhost:5173
```

Drop new example components into `src/dev/ExampleComponents/` and wire
them into `src/dev/App.tsx`'s `EXAMPLES` array.

### Building the library

```bash
yarn build         # vite build → dist/htmlToFigma.{js,umd.cjs}
                   # then tsc -p tsconfig.build.json → dist/*.d.ts
yarn preview       # serves the built bundles for smoke-testing
```

`vite.config.ts` marks `opentype.js`, `kiwi-schema`, and `pako` as
external — the consumer's bundler resolves them from `node_modules` so
we don't duplicate runtime deps.

### Linting

```bash
yarn lint
```

### Debugging scripts

Node-only scripts under `scripts/` help inspect the binary format:

| Script | What it does |
|---|---|
| `yarn generate-fixture <slug>` | Reads `fixtures/<slug>.input.html`, runs the generator, writes `fixtures/<slug>.generated.html`. `pbcopy < fixtures/<slug>.generated.html` to paste-test. |
| `yarn decode-fixture <slug>` | Parses an archive and dumps the JSON node tree. Great for seeing what the canonical Figma output produces. |
| `yarn decode-blob <slug> <n>` | Decodes a single glyph/vector blob out of an archive's blob table. |
| `yarn extract-schema <slug>` | Pulls the embedded kiwi schema from a fixture and rewrites `src/lib/figmaSchema.ts`. Only needed when Figma bumps the clipboard schema. |
| `yarn roundtrip-fixture <slug>` | Decodes and re-encodes a fixture, verifying our archive reader/writer stays byte-reversible. |
| `yarn test-fixture <slug>` | Runs the generator against a fixture's saved input HTML. |
| `yarn verify-generated <slug>` | Sanity-checks a generated payload parses and has no obvious schema violations. |

## Project layout

```
src/lib/           ★ everything that ships
src/dev/           ✗ Vite playground (never published)
fixtures/          ✗ captured input + reference HTML pairs (never published)
scripts/           ✗ debug utilities (never published)
assets/            ✗ bundled Inter TTFs for Node-side tests (never published)
.reference/        ✗ committed reference material, not published
dist/              ★ build output (gitignored, generated by `yarn build`)
```

Three layers keep non-ship files out of published tarballs:

1. `package.json` `"files": ["dist"]` — npm only publishes allowlisted
   entries. Everything else is already invisible to `npm publish`.
2. `.npmignore` — explicit deny list as a second pass.
3. `prepublishOnly` script — runs `yarn build` and asserts
   `.reference/` did not leak into `dist/` before publish proceeds.

Verify what would ship with `npm pack --dry-run`.

## Publishing

### TL;DR

```bash
# from packages/html-to-figma/
# 1. bump the version
npm version patch      # or: minor | major | <exact-version>
# 2. preview what will ship
npm pack --dry-run
# 3. publish
npm publish --access restricted
```

That's it. **You do NOT need to run `yarn build` manually** — the
`prepublishOnly` hook runs it automatically and verifies the output
before anything hits the registry.

### Step-by-step

1. **Make sure main is clean and `yarn` is up to date.**

   ```bash
   git status          # nothing uncommitted
   yarn install        # if package.json dependencies changed recently
   ```

2. **Bump the version.** `npm version` edits `package.json` and creates
   a version-bump commit + git tag in one go.

   ```bash
   npm version patch        # 0.0.36 → 0.0.37
   # npm version minor      # 0.0.36 → 0.1.0
   # npm version major      # 0.0.36 → 1.0.0
   # npm version 0.1.2      # exact
   ```

3. **Preview the tarball contents.** Confirms only `dist/` + `README.md`
   + `package.json` ship — no `fixtures/`, no `.reference/`, no
   `scripts/`.

   ```bash
   npm pack --dry-run
   ```

4. **Publish.**

   ```bash
   npm publish --access restricted
   ```

   What happens under the hood when you run `publish`:

   1. npm fires the `prepublishOnly` hook, which runs:
      - `yarn build` (clean rebuild of `dist/`)
      - a sanity check that `dist/htmlToFigma.js` exists
      - a sanity check that `dist/.reference/` did **not** leak in
   2. npm packs the allowlisted files (`"files": ["dist"]` in
      `package.json`, plus `README.md` + `package.json` by default).
   3. npm uploads the tarball to the registry.

   If `prepublishOnly` throws, **fix the underlying issue and retry** —
   don't `npm publish --force` past it. The checks exist to prevent
   broken or leaky releases.

5. **Push the version commit + tag.**

   ```bash
   git push --follow-tags
   ```

## Reference material

`.reference/` contains committed-but-unpublished source from the
upstream Figma export tool we reverse-engineered. Read the files there
(especially the `chunk-*.pretty.js` extracts) when you need to
understand a behaviour the canonical exporter produces. See
[.reference/README.md](./.reference/README.md).
