Files
platform/todo/backburner/m3e-canvas-as-a-plugin.md
pastilhasandClaude Fable 5 b99a1e7443 todo: m3e upstream said no — vendoring is the path
issue #55 and PR #56 both closed unmerged. two reasons, and the first
one is ours: the PR wired one of three clipboard writes and left
Inspector and PromptPanel writing directly, so it added a public surface
without delivering the thing it was for. the issue text named all three
sites and the PR wired one.

the second is theirs and fair — embedding is not on their roadmap and it
is a single-user need so far.

what survives is worth more than the merge would have been: they said
the adapter design is sound and they would start from it, and named what
a complete version needs — the Canvas extraction plus every export path.
that is a specification for the fork.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 02:47:20 +01:00

160 lines
8.3 KiB
Markdown

---
id: backburner/m3e-canvas-as-a-plugin
title: "M3E Canvas as a plugin — sketch a UI, hand the prompt to the agent"
theme: backburner
status: open
created: 2026-09-04
date_source: stated
order: 4
---
**M3E Canvas as a plugin.** [github.com/lnkiai/m3e-canvas](https://github.com/lnkiai/m3e-canvas) — a
browser design tool for Material 3 Expressive screens that exports the design as a natural-language
prompt for an AI coding tool. MIT.
The upstream question is now **answered** — see below; vendoring is the path. The rest is still open.
This card holds the measurements so the decision is made from numbers rather than from a README. Everything below was read from the code at **`cfde698`, 2026-09-04**.
## Why it is interesting
Its output is *a prompt you paste into Claude*. Officer is the thing that removes the paste — the export
button could open a chat session with the prompt already in it, pointed at a project directory on this
machine. **That last step is the only part Officer uniquely adds, and it is the whole reason to bother.**
The canvas already works standalone today.
## What it actually is
| | |
| --- | --- |
| size | 65 files, **14,822 lines** of TS/TSX |
| deps | `next` 16.3.4, `react` 19.2.8, `motion` ^13.1.1, `html-to-image` ^1.11.13 |
| licence | MIT |
| storage | `localStorage` only, no backend |
**Next.js is not actually there.** The entire codebase imports it **once**, and it is a type-only import:
`import type { Metadata, Viewport } from "next"` in `app/layout.tsx`. Eighteen files carry `'use client'`.
So it is a client-side React SPA that happens to be scaffolded in Next — there is nothing to strip, and
porting means giving it a different mount point.
That is the finding that changes the estimate. An earlier read of the README concluded "weeks of work,
own a fork forever", and that was wrong.
## The fork line falls on a file boundary
`app/page.tsx` imports about **twenty modules, every one a named export** through the `@/` alias, and
reaches into no internals. `lib/tokens.ts` is the shared spine with 16 importers. `app/` holds exactly
one file that matters.
| | lines | |
| --- | --- | --- |
| `app/page.tsx` | 3,433 | the whole editor: 15 components, 103 hook calls. **Yours.** |
| `lib/ai.ts` | ~200 | **Delete it** — stores provider API keys in `localStorage`. Officer has a secret store and the agent. |
| `lib/tokens.ts` | 1,614 | Material 3 tokens. Data. Vendor. |
| `components/M3Node.tsx` | 1,233 | the component renderer. Vendor. |
| `components/Inspector.tsx` | 1,171 | Vendor. |
| `lib/prompt.ts` | 1,151 | the export engine. Vendor. |
| `components/Preview.tsx` | 899 | Vendor. |
| `components/ui.tsx` | 882 | Vendor. |
| rest | ~4,200 | `Mobile`, `i18n`, `tidy`, `Toolbar`, `color`, `shapes`, `theme`, `project`. Vendor. |
**~3,600 lines owned, ~11,200 vendored** — and the split means something. What is vendored is *"what a
Material 3 component looks like and how to describe it in words"*, where upstream's value accumulates and
we have no wish to innovate. What is owned is *"where does this live in the workspace, whose is it, and
what happens on export"*, where upstream can never help because it does not know Officer exists.
**So deep integration and tracking upstream are not in tension.** The integration work lives entirely in
the one file that was always going to be rewritten.
## The export point is clean
```ts
buildPrompt(doc: Doc, widths: Record<string, number>, onlyFrameId?: string, lang: Lang): string
effectivePrompt(doc, widths, lang): string
```
Two pure functions in `lib/prompt.ts`. `Doc` in, string out, no side effects. No `postMessage` bridge and
no clipboard interception needed — **call the function.** Today it ends in
`navigator.clipboard.writeText` in three places; that is what gets replaced.
`Doc` is plain serialisable JSON (`lib/project.ts` already downloads and reloads it), so persisting to a
plugin table instead of `localStorage` is nearly free. Current keys: `DOC_KEY` and `UI_KEY` in
`page.tsx`, `m3e:ai`, and `m3e:sec:<id>`.
## The upstream problem, measured
This is the part that makes it a backburner card rather than a build.
- **37 commits, 3 days old** — first 2026-09-02, and **22 of them landed on 2026-09-04**, the day this
was read.
- **Two human authors** (`lnkiai` 25, `CQMHV` 10) plus dependabot.
It is under heavy active development *right now*. The export surface will churn, and no amount of
discipline on our side changes that. "Vendor it and track upstream" assumes an upstream that has settled,
and this one has not started.
### If it is vendored anyway
`web/vendor/m3e/`, committed with upstream URL, commit SHA and date in a README beside it. **The
precedent is already in the tree** — `plugins/jellyfin/web/vendor/` and `public/vendor/pako/`.
The piece that makes it survivable is **an adapter**: Officer's panels import from one `adapter.ts` that
re-exports from `vendor/`, never from `vendor/` directly. An upstream rename then costs one file instead
of twenty. Updating is: re-copy, read the diff, fix the seam.
## Asked upstream, 2026-09-04. Declined — vendoring is the path
[Issue #55](https://github.com/lnkiai/m3e-canvas/issues/55) asked for the seam;
[PR #56](https://github.com/lnkiai/m3e-canvas/pull/56) implemented the smaller half of it. Both closed
unmerged on 2026-09-05. The reply is worth reading in full, but the two reasons were:
1. **The PR did not actually unblock embedding.** It wired one of the three clipboard writes —
`app/page.tsx` — and left `components/Inspector.tsx:443` and `components/PromptPanel.tsx:35`
writing directly. So it "adds a public surface I'd have to keep stable" without delivering the
thing it was for. **That was our error, not a judgement call**: the issue text named all three
sites and the PR wired one.
2. **Embedding is not on their roadmap, and it is a single-user need so far.** Fair on both counts.
**What they said they would want** if that changes, quoted because it is the specification for any
future attempt:
> If I decide to support embedding I'd want to do it in one piece: the `Canvas` extraction plus the
> adapter wired into every export path. Your design for the adapter is sound and I'd start from it
> then.
So the adapter design survives — it is validated by the person who would have to maintain it — and the
work it needs is known: **the file move plus all three export paths**, not one of them.
The branch is on the fork at [andrepadez/m3e-canvas `embeddable-canvas`](https://github.com/andrepadez/m3e-canvas/tree/embeddable-canvas),
two commits, typecheck and build green. Worth keeping: if this is vendored, that is the diff to apply
on top, extended to the other two call sites.
### What this settles
**Vendor, do not depend.** The upstream door is open in principle and shut in practice, so the fork is
the only route to the integration — which is what the split below was already designed for. Nothing
about the file-boundary analysis changes; only the hope of upstreaming the shell does.
## If it is picked up anyway
**Open an issue asking upstream to make the seam explicit.** If `page.tsx` grew an entry point taking the
persistence layer and the export sink — `<Canvas storage={…} onExport={…} />` — we would **depend rather
than fork**, and the 3,600 owned lines shrink to a few hundred.
**Answered: no, for now.** Recorded above. Asking cost an afternoon and bought a specification for
what upstream would accept, plus confirmation that the adapter shape is right — which is a good return
on a declined PR.
## Open questions
1. **Is this a design tool that lives in Officer, or the front end of "sketch it and the agent builds
it"?** If the second, `page.tsx` is where we deliberately diverge, and adopting the fork is the right
frame from day one rather than something discovered in six months.
2. **Whose designs are they?** `Doc` is per-user data. Multi-user makes that a permissions question, and
the plugin table answers it in a way `localStorage` cannot.
3. **Does the phone editor survive the port?** `components/Mobile.tsx` (534 lines) is a separate
phone-optimised editor. Officer's mobile story is a native app, so this may be redundant or may be the
best part.
4. **An iframe was considered and rejected.** It buys a day and costs the function call, the theming, the
persistence, and forces back the clipboard round-trip the whole idea exists to remove. Recorded so it
is not re-proposed as the cheap option.