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>
160 lines
8.3 KiB
Markdown
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.
|