truecanvas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/catalog-worker.js +7 -0
- package/dist/chunk-753N4QRO.js +317 -0
- package/dist/chunk-J6CJBZO6.js +23 -0
- package/dist/chunk-WB35FXGP.js +3366 -0
- package/dist/cli.js +2931 -0
- package/dist/core/index.d.ts +820 -0
- package/dist/core/index.js +71 -0
- package/dist/editor/assets/controls-BQYSgX53.css +1 -0
- package/dist/editor/assets/controls-BYFjuGCq.js +9 -0
- package/dist/editor/assets/hub-C4_700Hv.js +2 -0
- package/dist/editor/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/editor/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/editor/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/editor/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/editor/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/editor/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/editor/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/editor/assets/main-CfAJbDf1.js +25 -0
- package/dist/editor/assets/react-DB-4Zxce.js +1 -0
- package/dist/editor/favicon.svg +3 -0
- package/dist/editor/hub.html +16 -0
- package/dist/editor/index.html +16 -0
- package/dist/next/index.d.ts +11 -0
- package/dist/next/index.js +83 -0
- package/dist/next/loader.cjs +16210 -0
- package/dist/runtime/host.d.ts +23 -0
- package/dist/runtime/host.js +412 -0
- package/dist/runtime/index.d.ts +42 -0
- package/dist/runtime/index.js +11 -0
- package/package.json +108 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Truecanvas contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/Nathandona/truecanvas/main/docs/logo.svg" width="56" alt="Truecanvas logo" />
|
|
2
|
+
|
|
3
|
+
# Truecanvas
|
|
4
|
+
|
|
5
|
+
**A Figma-like canvas for your real React components. The code is the document.**
|
|
6
|
+
|
|
7
|
+
[truecanvas.dev](https://truecanvas.dev)
|
|
8
|
+
|
|
9
|
+
Truecanvas renders your actual `.tsx` components on an infinite canvas, lets you compose them visually, and writes every change back to plain TSX in your repo. There is no built-in AI: it is **agent-first through MCP**, so Claude Code, Cursor, Codex or any MCP client can read your design system, edit frames and take screenshots, while you watch and steer in the editor.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
- **Real components, not drawings.** Layers are instances of your components. The props panel is generated from their TypeScript types (`status: "working" | "idle"` becomes a dropdown).
|
|
14
|
+
- **Code is the document.** A canvas is `canvas/<name>.canvas.tsx`. Edits are minimal text patches that keep your formatting, so diffs are reviewable and your editor and the canvas stay in sync both ways.
|
|
15
|
+
- **Agent-first.** 39 MCP tools: read the tree, list components and design tokens, insert JSX, set props, wrap in auto layout, create variant grids, add shader backgrounds, screenshot frames, and see what the user has selected.
|
|
16
|
+
- **Shaders.** Install [`@paper-design/shaders-react`](https://github.com/paper-design/shaders) and its ~30 animated shaders show up in the Components panel, with color pickers, sliders from their documented ranges and one-click presets. Add one behind any frame with "Shader background". Pause all motion with `⇧P` to save battery.
|
|
17
|
+
- **Design on your real pages.** "Add a page from your app" (Pages +) adds any App Router page as a **linked frame**: its layers are the page's own JSX, and editing them edits `page.tsx` (and the layout, for things like the navbar) directly. Right-click → **Explore a copy** to try ideas without touching the page, then **Apply to page** to write the result back as a minimal diff.
|
|
18
|
+
- **Build with plain elements and components.** The Insert menu (+ in the toolbar, `T` for text) and the Basics in Assets add text, headings, links, buttons, lists, stacks, rows, grids, sections, images and inputs as Tailwind-styled JSX. **New component** creates a component file; select layers and press `Ctrl/⌘+Alt+K` to turn them into a component (imports come along). Right-click any component → **Edit main component** opens it on the `components` page: its layers are the component's own JSX, edits go to its file, and every instance follows. `className={cn("…", className)}` stays merged: only the static classes change.
|
|
19
|
+
- **Pages, layers, assets.** Pages are canvas files you can add, rename, duplicate and delete (with undo). Layers have search, editor-only hide and lock, and custom names stored as `data-name` so your agent sees them too. Assets shows live thumbnails of your components, rendered by your app, plus every shader preset.
|
|
20
|
+
- **Motion.** Play a frame (▶ on its label) to scroll inside it at screen height, so scroll reveals run like on the site, and replay entrance animations anytime. Paused frames and agent screenshots show every animation finished, never half faded. The Motion section adds a scroll reveal (fade up, blur in, scale in…) or a text animation (word by word, letter by letter, typewriter…) to any layer: Truecanvas adds `Reveal` and `TextAnimate` to your components once (plain React, no dependencies) and wraps the layer in them.
|
|
21
|
+
- **Mobile presets.** iPhone, Pixel, Galaxy, iPad and desktop frame sizes, with device chrome (status bar, Dynamic Island, home indicator) and touch emulation in agent screenshots.
|
|
22
|
+
- **Everything is undoable.** Edits from you, your agent and your code editor land in one activity feed with undo/redo.
|
|
23
|
+
- **Light, dark, system.** Per-frame theme (Inherit / Light / Dark) and a canvas-wide theme, applied live without reloading frames.
|
|
24
|
+
- **Lightweight.** No second dev server. Frames render through your existing `next dev`, the editor is a static bundle, and headless Chromium only starts when an agent asks for a screenshot.
|
|
25
|
+
|
|
26
|
+
> Status: early (v0.1). Next.js App Router + Tailwind first. Linux-first, tested on Fedora 43.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
**In a Next.js app** (App Router, Next 15.3+):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx truecanvas
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's it. The first time, it asks once, then adds the dev dependency, wraps `next.config`, adds a `canvas` script, links your homepage as the first canvas and offers to connect your agents. Then it starts your app (if it isn't running) and opens the editor. After that, anyone on the project starts it with `npm run canvas`.
|
|
37
|
+
|
|
38
|
+
**A new app:**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm create truecanvas@latest my-app
|
|
42
|
+
cd my-app && npm run canvas
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A Next.js app (TypeScript, Tailwind, App Router) with Truecanvas set up and committed. `pnpm create truecanvas`, `yarn create truecanvas` and `bun create truecanvas` work too.
|
|
46
|
+
|
|
47
|
+
**Something off?** `npx truecanvas doctor` checks Node, Next, React, Tailwind, the config, agents, screenshots and git, with a one-line fix for each problem.
|
|
48
|
+
|
|
49
|
+
**Prefer an app to a terminal?** `npx truecanvas open` opens the Truecanvas window: your projects, one tab each, "Clone from GitHub" and "New project". `npx truecanvas desktop` adds it to your app launcher (Linux).
|
|
50
|
+
|
|
51
|
+
### Connect your agent
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx truecanvas connect
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This writes the Truecanvas MCP server into the project's **`.mcp.json`** (and `.cursor/mcp.json` / `.vscode/mcp.json` when the project uses those editors). **Commit it**: everyone who clones the repo gets Truecanvas in Claude Code, Cursor or VS Code with no setup.
|
|
58
|
+
|
|
59
|
+
| Client | Config |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Claude Code | `.mcp.json` from `truecanvas connect`, or for all your projects: `claude mcp add --scope user --transport http truecanvas http://localhost:4800/mcp` |
|
|
62
|
+
| Cursor | `.cursor/mcp.json` → `{ "mcpServers": { "truecanvas": { "url": "http://localhost:4800/mcp" } } }` |
|
|
63
|
+
| VS Code | `.vscode/mcp.json` → `{ "servers": { "truecanvas": { "type": "http", "url": "http://localhost:4800/mcp" } } }` |
|
|
64
|
+
| Codex | `~/.codex/config.toml` → `[mcp_servers.truecanvas]` `url = "http://localhost:4800/mcp"` |
|
|
65
|
+
| stdio-only clients | `{ "command": "npx", "args": ["truecanvas", "mcp"] }` (bridges to a running server, or starts one) |
|
|
66
|
+
|
|
67
|
+
Then select something on the canvas and ask: *"Make my selection denser"*, *"Create a dark copy of the Chat frame and check it with a screenshot"*, *"Show every status of SessionRow"*.
|
|
68
|
+
|
|
69
|
+
## The Truecanvas window
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx truecanvas desktop # adds Truecanvas to your app launcher (GNOME, KDE…)
|
|
73
|
+
npx truecanvas hub # or start it from a terminal
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
One window for all your projects, with tabs like a browser. The **Dashboard** lists your recent projects (with their favicons) and the Next.js apps it finds on your computer. You can open a folder, set up Truecanvas in an existing app, or create a new project. Each tab is a project's editor. Opening a tab starts that project's app on free ports, and closing it stops the app and frees the memory (the tab bar shows how much is in use).
|
|
77
|
+
|
|
78
|
+
The hub serves MCP at the same `http://localhost:4800/mcp`, and your agent works on the project in the active tab. It can also call `list_projects` and `open_project` to switch itself.
|
|
79
|
+
|
|
80
|
+
## Collaborate
|
|
81
|
+
|
|
82
|
+
Everything Truecanvas makes is a file in your repo: canvases, comments, linked pages, the motion components they use, and `.mcp.json`. So git is the collaboration layer, and a team works on design the way it works on code.
|
|
83
|
+
|
|
84
|
+
### The workflow
|
|
85
|
+
|
|
86
|
+
1. **Set up once, commit it.** `npx truecanvas` in the repo, then commit `package.json`, `canvas/`, `next.config` and `.mcp.json`. Teammates run their usual install and `npm run canvas`; their agents are already connected.
|
|
87
|
+
2. **Design on real pages.** Linked frames are your production pages: editing them edits `page.tsx`. For bigger ideas, right-click → **Explore a copy**, iterate freely (you or your agent), then **Apply to page**. Delete explorations once applied.
|
|
88
|
+
3. **One branch per change.** On `main`, Truecanvas offers to start a branch and brings your edits along.
|
|
89
|
+
4. **Commit from the Git panel.** It lists what changed per frame in plain words and drafts the message.
|
|
90
|
+
5. **Open a pull request** from the panel: the description shows each changed frame, before and after.
|
|
91
|
+
6. **Review in Truecanvas too.** In the Truecanvas window, a project's menu → **Pull requests…** checks out any PR and shows it on the canvas, live, not just as pictures. Leave comments on frames; they're saved in the branch, and agents can read and resolve them.
|
|
92
|
+
7. **Merge.** There's no handoff: the design is already the code.
|
|
93
|
+
|
|
94
|
+
Designers who don't use a terminal can do all of it from the Truecanvas window: **Clone from GitHub** (clones, installs, sets up), open the project, branch, commit, open and review PRs.
|
|
95
|
+
|
|
96
|
+
### What's in the Git panel
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
- **Git panel** (the branch pill at the top left): what changed, frame by frame, in plain words ("Restyled h1 (+text-orange-600)", "Added a fade-up reveal to p"), with a commit message drafted from it. Changed frames get a dot on the canvas. Design commits include canvases, comments, linked pages and the motion components they use.
|
|
100
|
+
- **Review changes**: a full-screen before/after of each changed frame against the last commit (rendered by your app), plus the code diff.
|
|
101
|
+
- **Branch first**: on `main` with changes, Truecanvas offers to start a branch (named from the page and frames) and carries your edits over.
|
|
102
|
+
- **Pull requests** (GitHub, through the `gh` CLI): push and open a PR in one step. Its description lists the changes per frame, optionally with before/after images stored on a `truecanvas-previews` branch of the same repo. The branch pill then shows the PR's state: open, checks running or failing, approved, merged. No GitHub remote yet? "Publish" creates the repo (private by default).
|
|
103
|
+
- **Compare**: render this page as it was on another branch or commit, side by side or as an overlay with an opacity slider. Changed and new frames are flagged. This works because Truecanvas renders any version of the code.
|
|
104
|
+
- **Comments** (`C`): pin a comment on a frame or layer. Threads are saved in `canvas/<page>.comments.json`, so they travel with branches and show up in pull requests. Agents read them with `list_comments` (including the current source of the commented layer), fix things, and `resolve_comment` with a note.
|
|
105
|
+
|
|
106
|
+
### Good to know
|
|
107
|
+
|
|
108
|
+
- **Async, like code.** Collaboration happens through branches and pull requests. Live multiplayer on one canvas isn't supported; the live cursors you see are agents working on your machine.
|
|
109
|
+
- **Merges.** Canvas files are TSX with one block per frame and minimal edits, so two people changing different frames merge cleanly. When both change the same frame, resolve it like any code conflict and Truecanvas re-renders the result.
|
|
110
|
+
- **One agent config for everyone.** `.mcp.json` points at `http://localhost:4800/mcp`, which works whether each person runs `npm run canvas` or the Truecanvas window.
|
|
111
|
+
|
|
112
|
+
## Linked frames
|
|
113
|
+
|
|
114
|
+
A new project's first canvas links your homepage. Any page can be linked from Pages + or with the `import_page` tool:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<Frame name="Home" x={0} y={0} width={1440} page="src/app/(marketing)/page.tsx">
|
|
118
|
+
<MarketingLayout>
|
|
119
|
+
<HomePage />
|
|
120
|
+
</MarketingLayout>
|
|
121
|
+
</Frame>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- The frame renders the real page through its layouts. Its layers are the JSX of `page.tsx` and of the layouts around it, and every edit goes to the file that layer lives in, keeping that file's formatting. The frame label shows **Live · page.tsx**.
|
|
125
|
+
- Undo, the Git panel (linked pages count as design files) and Compare (the page is rendered as it was at that commit) all work on the page files.
|
|
126
|
+
- Only plain composition is editable: a page whose component fetches data or has logic before its `return` is shown as one layer, **View only**. Explore a copy to design on it.
|
|
127
|
+
- **Explore a copy** creates `<Frame from="src/app/…/page.tsx">` with the page's JSX inline, in the canvas file. **Apply to page** replaces the page's returned JSX with the exploration's, adds the imports it needs and removes the ones it no longer uses.
|
|
128
|
+
- Page and layout layers get ids like `src/app/page.tsx#12:8`, stamped only in `next dev`. Production builds are untouched.
|
|
129
|
+
|
|
130
|
+
## A canvas file
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
"use client";
|
|
134
|
+
import { Canvas, Frame } from "truecanvas";
|
|
135
|
+
import { SessionRow } from "@/components/app/session-row";
|
|
136
|
+
|
|
137
|
+
export default function ChatCanvas() {
|
|
138
|
+
return (
|
|
139
|
+
<Canvas>
|
|
140
|
+
<Frame name="Sessions" x={0} y={0} width={320}>
|
|
141
|
+
<div className="flex flex-col gap-1 p-3">
|
|
142
|
+
<SessionRow title="Review release readiness" status="working" elapsed="2m" active />
|
|
143
|
+
<SessionRow title="Draft the Q4 changelog" status="review" source="slack" elapsed="3h" />
|
|
144
|
+
</div>
|
|
145
|
+
</Frame>
|
|
146
|
+
<Frame name="Sessions dark" x={360} y={0} width={320} theme="dark">
|
|
147
|
+
{/* … */}
|
|
148
|
+
</Frame>
|
|
149
|
+
</Canvas>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `<Frame>`: `name`, `x`, `y`, `width`, optional `height` (omit to hug content), optional `theme` (`"light" | "dark"`, omit to inherit), optional `device` (`"iphone-16"`, `"pixel-9"`, `"ipad-mini"`…), optional `page` (linked frame) or `from` (exploration of a page).
|
|
155
|
+
- Each frame renders in its own iframe at its own width, so media queries, portals and `position: fixed` behave like the real app.
|
|
156
|
+
- Keep props literal (`status="working"`) to make them editable. Expressions such as `{items.map(…)}` still render, but they are read-only in the inspector.
|
|
157
|
+
|
|
158
|
+
## MCP tools
|
|
159
|
+
|
|
160
|
+
| Read | Write | Look & show |
|
|
161
|
+
| --- | --- | --- |
|
|
162
|
+
| `list_canvases` | `set_props` · `set_text` · `set_class_name` | `screenshot_frame` |
|
|
163
|
+
| `get_canvas` (compact outline with ids) | `insert_jsx` · `replace_node` | `focus` (select + zoom in the user's editor) |
|
|
164
|
+
| `get_node` (exact TSX source) | `duplicate_nodes` · `delete_nodes` · `move_node` | `undo` · `redo` |
|
|
165
|
+
| `list_components` · `get_component` | `wrap_nodes` (auto layout) | |
|
|
166
|
+
| `get_design_tokens` (Tailwind `@theme`) | `create_frame` · `update_frame` (both take `device`) | |
|
|
167
|
+
| | `create_canvas` · `rename_canvas` · `delete_canvas` | |
|
|
168
|
+
| `get_selection` (what the user selected) | `create_variants_frame` · `add_background` · `apply_preset` | |
|
|
169
|
+
| `list_comments` | `reply_comment` · `resolve_comment` | |
|
|
170
|
+
| | `import_page` (linked by default) · `explore_copy` · `apply_to_page` | |
|
|
171
|
+
| | `add_animation` · `remove_animation` | `play_frame` (play, replay, stop) |
|
|
172
|
+
| | `create_component` · `open_component` | |
|
|
173
|
+
|
|
174
|
+
Node ids are `line:col` of the JSX tag (`file#line:col` for layers of a linked page) and change after edits. Every write returns the fresh ids of the affected frame. Catalog components are imported automatically.
|
|
175
|
+
|
|
176
|
+
## How it works
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
your browser truecanvas dev (Node, 127.0.0.1:4800)
|
|
180
|
+
┌───────────────────────────┐ REST/SSE ┌──────────────────────────────────┐
|
|
181
|
+
│ editor UI (static bundle) │◀─────────▶│ Workspace: parse → patch → write │◀── MCP ── your agent
|
|
182
|
+
│ ┌──────┐ ┌──────┐ │ │ (Babel + magic-string, undo log) │
|
|
183
|
+
│ │iframe│ │iframe│ frames │ │ Catalog: TS checker → prop types │
|
|
184
|
+
│ └──┬───┘ └──┬───┘ │ └───────────────┬──────────────────┘
|
|
185
|
+
└─────┼────────┼────────────┘ │ writes canvas/*.canvas.tsx
|
|
186
|
+
│ └── postMessage: hit-test, rects, theme ▼
|
|
187
|
+
└──────────── /truecanvas/[canvas]?frame=… ── your `next dev` (HMR)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
1. A Turbopack/webpack loader stamps each JSX element in `*.canvas.tsx` with `data-tc="line:col"` (dev only, same line, no layout impact).
|
|
191
|
+
2. In the iframe, a tiny runtime answers "what's under this point?" by walking React's fiber tree to the nearest stamped element. No wrapper elements, and it works with portals and components that don't forward props.
|
|
192
|
+
3. The editor turns your gestures into commands. The server patches the source text and writes it, and Next's HMR re-renders the frame.
|
|
193
|
+
4. Agents use the same commands over MCP, so whatever you can do, they can do.
|
|
194
|
+
|
|
195
|
+
## Configuration
|
|
196
|
+
|
|
197
|
+
Optional `truecanvas.config.json` at the project root:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"appUrl": "http://localhost:3000",
|
|
202
|
+
"port": 4800,
|
|
203
|
+
"canvasDir": "canvas",
|
|
204
|
+
"components": ["components/**/*.tsx"],
|
|
205
|
+
"darkMode": { "strategy": "class", "value": "dark" },
|
|
206
|
+
"libraries": ["@paper-design/shaders-react"],
|
|
207
|
+
"editor": "code"
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`libraries` adds npm packages to the Components panel (Paper shaders are detected automatically when installed). `darkMode` tells frames how your app switches themes: a class on `<html>` (`strategy: "class"`), or an attribute (`{ "strategy": "attribute", "attribute": "data-theme", "value": "dark", "lightValue": "light" }`).
|
|
212
|
+
|
|
213
|
+
## Shortcuts
|
|
214
|
+
|
|
215
|
+
| | |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| Select / Frame / Hand / Interact | `V` `F` `H` `P` |
|
|
218
|
+
| Duplicate · Delete | `⌘D` · `⌫` |
|
|
219
|
+
| Wrap in auto layout | `⇧A` |
|
|
220
|
+
| Reorder in parent | `↑` `↓` or drag on the canvas |
|
|
221
|
+
| Parent / child | `⇧↵` / `↵` (or `Esc`) |
|
|
222
|
+
| Edit text | double-click or `↵` |
|
|
223
|
+
| Rename layer or page | double-click its name |
|
|
224
|
+
| Copy as JSX | `⌘C` |
|
|
225
|
+
| Pause / play animations | `⇧P` |
|
|
226
|
+
| Comment | `C` |
|
|
227
|
+
| Zoom to fit · selection · 100% | `⇧1` · `⇧2` · `⇧0` |
|
|
228
|
+
| Undo · Redo | `⌘Z` · `⇧⌘Z` |
|
|
229
|
+
|
|
230
|
+
On Linux, `⌘` is `Ctrl`. Pan with space-drag, middle mouse or two-finger scroll. Zoom with Ctrl+scroll or pinch.
|
|
231
|
+
|
|
232
|
+
## Requirements
|
|
233
|
+
|
|
234
|
+
- Node 20+, Next.js 15.3+ (App Router), React 19. Tailwind is optional but the style controls write Tailwind classes.
|
|
235
|
+
- Linux and macOS. Windows is untested.
|
|
236
|
+
- For screenshots and PR images: Chrome/Chromium, or a Playwright browser in `~/.cache/ms-playwright`. Set `TRUECANVAS_CHROME` to point at one.
|
|
237
|
+
- For pull requests: the GitHub CLI (`gh auth login`).
|
|
238
|
+
|
|
239
|
+
`npx truecanvas doctor` checks all of this.
|
|
240
|
+
|
|
241
|
+
## Develop
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
pnpm install
|
|
245
|
+
pnpm build # library + editor bundle
|
|
246
|
+
pnpm --filter truecanvas test # engine tests
|
|
247
|
+
pnpm dev # builds, then starts the playground app + editor
|
|
248
|
+
pnpm --filter truecanvas dev:editor # editor with Vite HMR on :4801 (proxies the API)
|
|
249
|
+
pnpm pack:local # an installable build, plus the command to add it to a project
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Running the CLI from this checkout (`node packages/truecanvas/dist/cli.js`) installs a tarball of your local build into projects instead of the npm release.
|
|
253
|
+
|
|
254
|
+
**Releasing:** `pnpm release 0.2.0` bumps `truecanvas` and `create-truecanvas`, commits and tags; `git push --follow-tags` runs the release workflow, which tests and publishes both to npm with provenance (needs an `NPM_TOKEN` secret in the GitHub repo).
|
|
255
|
+
|
|
256
|
+
## Roadmap
|
|
257
|
+
|
|
258
|
+
- Multi-frame drag and reparenting by dragging on the canvas
|
|
259
|
+
- Vite + React Router support, then CSS Modules
|
|
260
|
+
- Inline prop editing for `ReactNode` slots
|
|
261
|
+
- Desktop shell (AppImage / Flatpak) once the web version settles
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeComponents
|
|
3
|
+
} from "./chunk-753N4QRO.js";
|
|
4
|
+
|
|
5
|
+
// src/core/catalog-worker.ts
|
|
6
|
+
import { parentPort, workerData } from "worker_threads";
|
|
7
|
+
analyzeComponents(workerData.root, workerData.globs, workerData.libraries).then((list) => parentPort.postMessage({ ok: true, list })).catch((err) => parentPort.postMessage({ ok: false, error: err.message }));
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// src/core/catalog.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { globSync } from "tinyglobby";
|
|
7
|
+
var require2 = createRequire(import.meta.url);
|
|
8
|
+
var Catalog = class {
|
|
9
|
+
constructor(root, globs, libraries = []) {
|
|
10
|
+
this.root = root;
|
|
11
|
+
this.globs = globs;
|
|
12
|
+
this.libraries = libraries;
|
|
13
|
+
}
|
|
14
|
+
root;
|
|
15
|
+
globs;
|
|
16
|
+
libraries;
|
|
17
|
+
cache = null;
|
|
18
|
+
loading = null;
|
|
19
|
+
generation = 0;
|
|
20
|
+
invalidate() {
|
|
21
|
+
this.generation++;
|
|
22
|
+
this.cache = null;
|
|
23
|
+
this.loading = null;
|
|
24
|
+
}
|
|
25
|
+
/** Cached components; call load() first. */
|
|
26
|
+
list() {
|
|
27
|
+
return this.cache ?? [];
|
|
28
|
+
}
|
|
29
|
+
get(name) {
|
|
30
|
+
return this.list().find((c) => c.name === name);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Analyzes components in a short-lived worker thread: the TypeScript program
|
|
34
|
+
* is large, and a worker gives all of that memory back when it exits.
|
|
35
|
+
*/
|
|
36
|
+
load() {
|
|
37
|
+
if (this.cache) return Promise.resolve(this.cache);
|
|
38
|
+
if (this.loading) return this.loading;
|
|
39
|
+
const gen = this.generation;
|
|
40
|
+
const job = runAnalysis(this.root, this.globs, this.libraries).then((list) => {
|
|
41
|
+
if (gen === this.generation) this.cache = list;
|
|
42
|
+
return list;
|
|
43
|
+
});
|
|
44
|
+
const pending = job.finally(() => {
|
|
45
|
+
if (this.loading === pending) this.loading = null;
|
|
46
|
+
});
|
|
47
|
+
pending.catch(() => {
|
|
48
|
+
});
|
|
49
|
+
this.loading = pending;
|
|
50
|
+
return job;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
async function runAnalysis(root, globs, libraries) {
|
|
54
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
const worker = [path.join(here, "catalog-worker.js"), path.join(here, "..", "catalog-worker.js")].find((p) => fs.existsSync(p));
|
|
56
|
+
if (!worker) return analyzeComponents(root, globs, libraries);
|
|
57
|
+
const { Worker } = await import("worker_threads");
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const w = new Worker(worker, { workerData: { root, globs, libraries } });
|
|
60
|
+
w.once("message", (m) => m.ok ? resolve(m.list) : reject(new Error(m.error)));
|
|
61
|
+
w.once("error", reject);
|
|
62
|
+
w.once("exit", (code) => code !== 0 && reject(new Error(`catalog worker exited with ${code}`)));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function analyzeComponents(root, globs, libraries = []) {
|
|
66
|
+
const ts = require2("typescript");
|
|
67
|
+
const files = globSync(globs, { cwd: root, absolute: true, ignore: ["**/node_modules/**", "**/*.canvas.tsx", "**/*.test.*", "**/*.stories.*"] });
|
|
68
|
+
const options = compilerOptions(ts, root);
|
|
69
|
+
const libEntries = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const lib of libraries) {
|
|
71
|
+
const res = ts.resolveModuleName(lib, path.join(root, "index.ts"), options, ts.sys).resolvedModule;
|
|
72
|
+
if (res) libEntries.set(res.resolvedFileName, lib);
|
|
73
|
+
}
|
|
74
|
+
const program = ts.createProgram({ rootNames: [...files, ...libEntries.keys()], options });
|
|
75
|
+
const checker = program.getTypeChecker();
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const file of [...files, ...libEntries.keys()]) {
|
|
78
|
+
const library = libEntries.get(file);
|
|
79
|
+
const sf = program.getSourceFile(file);
|
|
80
|
+
if (!sf) continue;
|
|
81
|
+
const moduleSymbol = checker.getSymbolAtLocation(sf);
|
|
82
|
+
if (!moduleSymbol) continue;
|
|
83
|
+
for (let sym of checker.getExportsOfModule(moduleSymbol)) {
|
|
84
|
+
if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym);
|
|
85
|
+
const name = sym.getName();
|
|
86
|
+
if (!/^[A-Z]/.test(name) || !(sym.flags & ts.SymbolFlags.Value)) continue;
|
|
87
|
+
const decl = sym.valueDeclaration ?? sym.declarations?.[0];
|
|
88
|
+
if (!decl) continue;
|
|
89
|
+
const type = checker.getTypeOfSymbolAtLocation(sym, decl);
|
|
90
|
+
const sig = type.getCallSignatures()[0];
|
|
91
|
+
if (!sig) continue;
|
|
92
|
+
const ret = checker.typeToString(sig.getReturnType());
|
|
93
|
+
if (!/Element|ReactNode|ReactPortal|null|any/.test(ret) || /^Promise</.test(ret)) continue;
|
|
94
|
+
out.push(describe(ts, checker, sym, decl, sig, library ?? path.relative(root, file), library));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const spec of out) applyProfile(spec);
|
|
98
|
+
if (libraries.length) await attachPresets(root, out);
|
|
99
|
+
out.sort((a, b) => (a.library ?? "").localeCompare(b.library ?? "") || a.name.localeCompare(b.name));
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
var LIBRARY_PROFILES = {
|
|
103
|
+
"@paper-design/shaders-react": {
|
|
104
|
+
hidden: ["minPixelRatio", "maxPixelCount", "webGlContextAttributes", "frame", "width", "height", "children", "style"],
|
|
105
|
+
advanced: ["fit", "scale", "rotation", "originX", "originY", "offsetX", "offsetY", "worldWidth", "worldHeight"]
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function applyProfile(spec) {
|
|
109
|
+
const profile = spec.library ? LIBRARY_PROFILES[spec.library] : void 0;
|
|
110
|
+
if (!profile) return;
|
|
111
|
+
spec.props = spec.props.filter((p) => !profile.hidden.includes(p.name));
|
|
112
|
+
if (profile.hidden.includes("children")) spec.acceptsChildren = false;
|
|
113
|
+
for (const p of spec.props) if (profile.advanced.includes(p.name)) p.advanced = true;
|
|
114
|
+
}
|
|
115
|
+
async function attachPresets(root, specs) {
|
|
116
|
+
const byLib = /* @__PURE__ */ new Map();
|
|
117
|
+
for (const s of specs) if (s.library) byLib.set(s.library, [...byLib.get(s.library) ?? [], s]);
|
|
118
|
+
const projectRequire = createRequire(path.join(root, "package.json"));
|
|
119
|
+
for (const [lib, list] of byLib) {
|
|
120
|
+
let mod;
|
|
121
|
+
try {
|
|
122
|
+
const { pathToFileURL } = await import("url");
|
|
123
|
+
mod = await import(pathToFileURL(projectRequire.resolve(lib)).href);
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const spec of list) {
|
|
128
|
+
const raw = mod[`${spec.name[0].toLowerCase()}${spec.name.slice(1)}Presets`];
|
|
129
|
+
if (!Array.isArray(raw)) continue;
|
|
130
|
+
const known = new Set(spec.props.map((p) => p.name));
|
|
131
|
+
spec.presets = raw.filter((p) => !!p && typeof p.name === "string" && typeof p.params === "object").map((p) => ({
|
|
132
|
+
name: p.name,
|
|
133
|
+
props: Object.fromEntries(Object.entries(p.params).filter(([k, v]) => known.has(k) && k !== "frame" && isLiteral(v)))
|
|
134
|
+
}));
|
|
135
|
+
const def = spec.presets.find((p) => p.name === "Default") ?? spec.presets[0];
|
|
136
|
+
if (def) {
|
|
137
|
+
for (const prop of spec.props) if (prop.default === void 0 && prop.name in def.props) prop.default = def.props[prop.name];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function isLiteral(v) {
|
|
143
|
+
if (Array.isArray(v)) return v.every((x) => ["string", "number", "boolean"].includes(typeof x));
|
|
144
|
+
return ["string", "number", "boolean"].includes(typeof v);
|
|
145
|
+
}
|
|
146
|
+
function compilerOptions(ts, root) {
|
|
147
|
+
const configPath = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
148
|
+
const base = { jsx: ts.JsxEmit.ReactJSX, strict: true, skipLibCheck: true, noEmit: true, allowJs: false };
|
|
149
|
+
if (!configPath) return base;
|
|
150
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
151
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath));
|
|
152
|
+
return { ...parsed.options, noEmit: true, skipLibCheck: true, incremental: false, plugins: [] };
|
|
153
|
+
}
|
|
154
|
+
function describe(ts, checker, sym, decl, sig, file, library) {
|
|
155
|
+
const props = [];
|
|
156
|
+
const param = sig.getParameters()[0];
|
|
157
|
+
const defaults = readDefaults(ts, decl);
|
|
158
|
+
let acceptsClassName = false;
|
|
159
|
+
let acceptsChildren = false;
|
|
160
|
+
if (param) {
|
|
161
|
+
const paramType = checker.getTypeOfSymbolAtLocation(param, decl);
|
|
162
|
+
for (const prop of paramType.getProperties()) {
|
|
163
|
+
const name = prop.getName();
|
|
164
|
+
if (name === "key" || name === "ref") continue;
|
|
165
|
+
const decls = prop.getDeclarations() ?? [];
|
|
166
|
+
const own = decls.some((d) => {
|
|
167
|
+
const f = d.getSourceFile().fileName;
|
|
168
|
+
return library ? !/node_modules\/(@types\/react|typescript)\//.test(f) && !f.includes("/csstype/") : !f.includes("node_modules");
|
|
169
|
+
});
|
|
170
|
+
if (name === "className") acceptsClassName = true;
|
|
171
|
+
if (name === "children") acceptsChildren = true;
|
|
172
|
+
if (!own && name !== "children") continue;
|
|
173
|
+
const propType = checker.getTypeOfSymbolAtLocation(prop, decls[0] ?? decl);
|
|
174
|
+
const spec = classify(ts, checker, propType, name);
|
|
175
|
+
if (spec.options) spec.options = declaredOrder(ts, checker, decls[0], spec.options);
|
|
176
|
+
const description = ts.displayPartsToString(prop.getDocumentationComment(checker)) || void 0;
|
|
177
|
+
const range = spec.type === "number" ? readRange(name, description, decls[0]) : void 0;
|
|
178
|
+
props.push({
|
|
179
|
+
name,
|
|
180
|
+
...spec,
|
|
181
|
+
...range,
|
|
182
|
+
optional: !!(prop.flags & ts.SymbolFlags.Optional),
|
|
183
|
+
default: defaults[name],
|
|
184
|
+
description
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
name: sym.getName(),
|
|
190
|
+
file,
|
|
191
|
+
library,
|
|
192
|
+
description: ts.displayPartsToString(sym.getDocumentationComment(checker)) || void 0,
|
|
193
|
+
props,
|
|
194
|
+
acceptsClassName,
|
|
195
|
+
acceptsChildren
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function readRange(name, description, decl) {
|
|
199
|
+
const re = /\((-?\d+(?:\.\d+)?)\s*(?:to|-|–|\.\.)\s*(-?\d+(?:\.\d+)?)\)/;
|
|
200
|
+
const fromDoc = description && re.exec(description);
|
|
201
|
+
if (fromDoc) return { min: Number(fromDoc[1]), max: Number(fromDoc[2]) };
|
|
202
|
+
const text = decl?.getSourceFile().text;
|
|
203
|
+
if (!text) return void 0;
|
|
204
|
+
const line = new RegExp(`\\bu_${name}\\b[^\\n]*`).exec(text)?.[0];
|
|
205
|
+
const m = line && re.exec(line);
|
|
206
|
+
return m ? { min: Number(m[1]), max: Number(m[2]) } : void 0;
|
|
207
|
+
}
|
|
208
|
+
function classify(ts, checker, type, name = "") {
|
|
209
|
+
const typeText = checker.typeToString(type, void 0, ts.TypeFormatFlags.NoTruncation);
|
|
210
|
+
const parts = type.isUnion() ? type.types : [type];
|
|
211
|
+
const real = parts.filter((p) => !(p.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void)));
|
|
212
|
+
if (/colou?rs$/i.test(name) && /string\[\]|readonly string\[\]/.test(typeText)) return { type: "colors", typeText };
|
|
213
|
+
if (/colou?r/i.test(name) && real.length && real.every((p) => p.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))) return { type: "color", typeText };
|
|
214
|
+
if (/ReactNode|ReactElement|JSX\.Element/.test(typeText)) return { type: "node", typeText };
|
|
215
|
+
if (real.length && real.every((p) => p.flags & ts.TypeFlags.BooleanLike)) return { type: "boolean", typeText };
|
|
216
|
+
if (real.length && real.every((p) => p.isStringLiteral())) {
|
|
217
|
+
return { type: "enum", options: real.map((p) => p.value), typeText };
|
|
218
|
+
}
|
|
219
|
+
if (real.length === 1 && real[0].flags & ts.TypeFlags.String) return { type: "string", typeText };
|
|
220
|
+
if (real.length === 1 && real[0].flags & ts.TypeFlags.Number) return { type: "number", typeText };
|
|
221
|
+
if (real.some((p) => p.getCallSignatures().length)) return { type: "function", typeText };
|
|
222
|
+
if (real.length && real.every((p) => p.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral))) return { type: "string", typeText };
|
|
223
|
+
if (real.length && real.every((p) => p.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral))) return { type: "number", typeText };
|
|
224
|
+
return { type: "other", typeText };
|
|
225
|
+
}
|
|
226
|
+
function declaredOrder(ts, checker, decl, options) {
|
|
227
|
+
let typeNode = decl && (ts.isPropertySignature(decl) || ts.isPropertyDeclaration(decl)) ? decl.type : void 0;
|
|
228
|
+
for (let hops = 0; typeNode && hops < 5; hops++) {
|
|
229
|
+
if (ts.isUnionTypeNode(typeNode)) {
|
|
230
|
+
const order = [];
|
|
231
|
+
for (const member of typeNode.types) {
|
|
232
|
+
if (ts.isLiteralTypeNode(member) && ts.isStringLiteral(member.literal)) order.push(member.literal.text);
|
|
233
|
+
else if (ts.isTypeReferenceNode(member)) {
|
|
234
|
+
const t = checker.getTypeFromTypeNode(member);
|
|
235
|
+
for (const u of t.isUnion() ? t.types : [t]) if (u.isStringLiteral()) order.push(u.value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const known = order.filter((o) => options.includes(o));
|
|
239
|
+
return [.../* @__PURE__ */ new Set([...known, ...options])];
|
|
240
|
+
}
|
|
241
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
242
|
+
const sym = checker.getSymbolAtLocation(typeNode.typeName);
|
|
243
|
+
const target = sym && sym.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(sym) : sym;
|
|
244
|
+
const alias = target?.declarations?.find(ts.isTypeAliasDeclaration);
|
|
245
|
+
typeNode = alias?.type;
|
|
246
|
+
} else if (ts.isParenthesizedTypeNode(typeNode)) typeNode = typeNode.type;
|
|
247
|
+
else break;
|
|
248
|
+
}
|
|
249
|
+
return options;
|
|
250
|
+
}
|
|
251
|
+
function readDefaults(ts, decl) {
|
|
252
|
+
let fn;
|
|
253
|
+
if (ts.isFunctionDeclaration(decl) || ts.isFunctionExpression(decl) || ts.isArrowFunction(decl)) fn = decl;
|
|
254
|
+
else if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
255
|
+
let init = decl.initializer;
|
|
256
|
+
while (ts.isCallExpression(init) && init.arguments[0]) init = init.arguments[0];
|
|
257
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) fn = init;
|
|
258
|
+
}
|
|
259
|
+
const out = {};
|
|
260
|
+
const first = fn?.parameters[0];
|
|
261
|
+
if (!first || !ts.isObjectBindingPattern(first.name)) return out;
|
|
262
|
+
for (const el of first.name.elements) {
|
|
263
|
+
if (!el.initializer) continue;
|
|
264
|
+
const key = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : ts.isIdentifier(el.name) ? el.name.text : null;
|
|
265
|
+
if (!key) continue;
|
|
266
|
+
const init = el.initializer;
|
|
267
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) out[key] = init.text;
|
|
268
|
+
else if (ts.isNumericLiteral(init)) out[key] = Number(init.text);
|
|
269
|
+
else if (init.kind === ts.SyntaxKind.TrueKeyword) out[key] = true;
|
|
270
|
+
else if (init.kind === ts.SyntaxKind.FalseKeyword) out[key] = false;
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function importSpecifier(root, canvasFile, componentFile) {
|
|
275
|
+
if (!/\.(tsx|ts|jsx|js)$/.test(componentFile) && !componentFile.startsWith(".")) return componentFile;
|
|
276
|
+
const abs = path.resolve(root, componentFile);
|
|
277
|
+
const noExt = abs.replace(/\.(tsx|ts|jsx|js)$/, "").replace(/\/index$/, "");
|
|
278
|
+
const alias = readAlias(root);
|
|
279
|
+
if (alias) {
|
|
280
|
+
const rel2 = path.relative(alias.dir, noExt);
|
|
281
|
+
if (!rel2.startsWith("..")) return `${alias.prefix}${rel2}`;
|
|
282
|
+
}
|
|
283
|
+
let rel = path.relative(path.dirname(path.resolve(root, canvasFile)), noExt);
|
|
284
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
285
|
+
return rel;
|
|
286
|
+
}
|
|
287
|
+
var aliasCache = null;
|
|
288
|
+
function readAlias(root) {
|
|
289
|
+
if (aliasCache?.root === root) return aliasCache.value;
|
|
290
|
+
let value = null;
|
|
291
|
+
try {
|
|
292
|
+
const ts = require2("typescript");
|
|
293
|
+
const configPath = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
294
|
+
if (configPath) {
|
|
295
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
296
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath));
|
|
297
|
+
const paths = parsed.options.paths ?? {};
|
|
298
|
+
const base = parsed.options.pathsBasePath ?? parsed.options.baseUrl ?? path.dirname(configPath);
|
|
299
|
+
for (const [key, targets] of Object.entries(paths)) {
|
|
300
|
+
if (key.endsWith("/*") && targets[0]?.endsWith("/*")) {
|
|
301
|
+
value = { prefix: key.slice(0, -1), dir: path.resolve(base, targets[0].slice(0, -2)) };
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
value = null;
|
|
308
|
+
}
|
|
309
|
+
aliasCache = { root, value };
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export {
|
|
314
|
+
Catalog,
|
|
315
|
+
analyzeComponents,
|
|
316
|
+
importSpecifier
|
|
317
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/runtime/index.tsx
|
|
4
|
+
import { createContext, useContext } from "react";
|
|
5
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
6
|
+
var HostContext = createContext(null);
|
|
7
|
+
function Canvas({ children }) {
|
|
8
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
9
|
+
}
|
|
10
|
+
function Frame({ name, width, height, children }) {
|
|
11
|
+
const host = useContext(HostContext);
|
|
12
|
+
if (host) {
|
|
13
|
+
if (host.frame !== name) return null;
|
|
14
|
+
return /* @__PURE__ */ jsx("div", { "data-tc-frame": name, style: { width: "100%", height: height ? "100vh" : void 0, display: "flow-root", position: "relative", isolation: "isolate" }, children });
|
|
15
|
+
}
|
|
16
|
+
return /* @__PURE__ */ jsx("div", { "data-tc-frame": name, style: { width, height, overflow: "hidden", position: "relative", display: "flow-root" }, children });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
HostContext,
|
|
21
|
+
Canvas,
|
|
22
|
+
Frame
|
|
23
|
+
};
|