truecanvas 0.1.0 → 0.2.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/README.md CHANGED
@@ -12,11 +12,12 @@ Truecanvas renders your actual `.tsx` components on an infinite canvas, lets you
12
12
 
13
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
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.
15
+ - **Agent-first.** 44 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
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
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
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
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
+ - **Icons and shadcn/ui.** Assets → Libraries (or + → Icon…) installs an icon set (Lucide, Tabler, Phosphor, Heroicons, Radix) with your package manager, then search and insert icons with their import. The shadcn/ui tab runs the official shadcn CLI to copy components into your project (setting shadcn up first if needed); they show up in Assets with their typed variants like any component. Your agent can do the same through MCP.
20
21
  - **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
22
  - **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
23
  - **Everything is undoable.** Edits from you, your agent and your code editor land in one activity feed with undo/redo.
@@ -170,6 +171,7 @@ export default function ChatCanvas() {
170
171
  | | `import_page` (linked by default) · `explore_copy` · `apply_to_page` | |
171
172
  | | `add_animation` · `remove_animation` | `play_frame` (play, replay, stop) |
172
173
  | | `create_component` · `open_component` | |
174
+ | `list_libraries` · `search_icons` | `install_library` · `insert_icon` · `add_shadcn_components` | |
173
175
 
174
176
  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
177
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  analyzeComponents
3
- } from "./chunk-753N4QRO.js";
3
+ } from "./chunk-T2TCBSTG.js";
4
4
 
5
5
  // src/core/catalog-worker.ts
6
6
  import { parentPort, workerData } from "worker_threads";
@@ -0,0 +1,196 @@
1
+ // src/core/icons.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { createRequire } from "module";
5
+ import { fileURLToPath, pathToFileURL } from "url";
6
+ var aliasOf = (name, all, suffix) => name.endsWith(suffix) && all.has(name.slice(0, -suffix.length));
7
+ var ICON_LIBRARIES = [
8
+ {
9
+ id: "lucide",
10
+ label: "Lucide",
11
+ package: "lucide-react",
12
+ from: "lucide-react",
13
+ homepage: "https://lucide.dev/icons",
14
+ pick: (n, all) => !n.startsWith("Lucide") && !aliasOf(n, all, "Icon") && n !== "Icon"
15
+ },
16
+ {
17
+ id: "tabler",
18
+ label: "Tabler Icons",
19
+ package: "@tabler/icons-react",
20
+ from: "@tabler/icons-react",
21
+ homepage: "https://tabler.io/icons",
22
+ pick: (n) => /^Icon[A-Z0-9]/.test(n)
23
+ },
24
+ {
25
+ id: "phosphor",
26
+ label: "Phosphor",
27
+ package: "@phosphor-icons/react",
28
+ from: "@phosphor-icons/react",
29
+ homepage: "https://phosphoricons.com",
30
+ pick: (n, all) => !["IconBase", "IconContext", "SSRBase"].includes(n) && !aliasOf(n, all, "Icon")
31
+ },
32
+ {
33
+ id: "heroicons",
34
+ label: "Heroicons outline",
35
+ package: "@heroicons/react",
36
+ from: "@heroicons/react/24/outline",
37
+ homepage: "https://heroicons.com",
38
+ pick: (n) => n.endsWith("Icon")
39
+ },
40
+ {
41
+ id: "heroicons-solid",
42
+ label: "Heroicons solid",
43
+ package: "@heroicons/react",
44
+ from: "@heroicons/react/24/solid",
45
+ homepage: "https://heroicons.com",
46
+ pick: (n) => n.endsWith("Icon")
47
+ },
48
+ {
49
+ id: "radix",
50
+ label: "Radix Icons",
51
+ package: "@radix-ui/react-icons",
52
+ from: "@radix-ui/react-icons",
53
+ homepage: "https://www.radix-ui.com/icons",
54
+ pick: (n) => n.endsWith("Icon")
55
+ }
56
+ ];
57
+ function iconLibrary(id) {
58
+ return ICON_LIBRARIES.find((l) => l.id === id || l.package === id || l.from === id);
59
+ }
60
+ function installedVersion(root, pkg) {
61
+ try {
62
+ const req = createRequire(path.join(root, "package.json"));
63
+ let dir = path.dirname(req.resolve(pkg));
64
+ for (let i = 0; i < 6; i++, dir = path.dirname(dir)) {
65
+ const file = path.join(dir, "package.json");
66
+ if (fs.existsSync(file)) {
67
+ const json = JSON.parse(fs.readFileSync(file, "utf8"));
68
+ if (json.name === pkg) return json.version ?? null;
69
+ }
70
+ }
71
+ } catch {
72
+ }
73
+ return null;
74
+ }
75
+ async function renderIconSet(root, id) {
76
+ const lib = iconLibrary(id);
77
+ if (!lib) throw new Error(`Unknown icon library "${id}".`);
78
+ const req = createRequire(path.join(root, "package.json"));
79
+ const load = async (spec) => import(pathToFileURL(req.resolve(spec)).href);
80
+ const React = await load("react");
81
+ const server = await load("react-dom/server");
82
+ const mod = await load(lib.from);
83
+ const createElement = React.createElement ?? React.default?.createElement;
84
+ const renderToStaticMarkup = server.renderToStaticMarkup ?? server.default?.renderToStaticMarkup;
85
+ const exports = { ...mod.default && typeof mod.default === "object" ? mod.default : {}, ...mod };
86
+ const all = new Set(Object.keys(exports).filter((n) => /^[A-Z]/.test(n)));
87
+ const out = [];
88
+ for (const name of [...all].sort()) {
89
+ if (!lib.pick(name, all)) continue;
90
+ const C = exports[name];
91
+ if (!(typeof C === "function" || typeof C === "object" && C !== null && "$$typeof" in C)) continue;
92
+ try {
93
+ const svg = renderToStaticMarkup(createElement(C, {}));
94
+ if (svg.startsWith("<svg")) out.push({ name, svg: svg.replace(/\s(?:class|data-[\w-]+)="[^"]*"/g, "") });
95
+ } catch {
96
+ }
97
+ }
98
+ return out;
99
+ }
100
+ function cacheFile(root, id, version) {
101
+ return path.join(root, "node_modules", ".cache", "truecanvas", "icons", `${id}@${version}.json`);
102
+ }
103
+ var memo = null;
104
+ var inflight = /* @__PURE__ */ new Map();
105
+ async function loadIcons(root, id) {
106
+ const lib = iconLibrary(id);
107
+ if (!lib) throw new Error(`Unknown icon library "${id}". Known: ${ICON_LIBRARIES.map((l) => l.id).join(", ")}.`);
108
+ const version = installedVersion(root, lib.package);
109
+ if (!version) throw new Error(`${lib.package} isn't installed in this project. Install it first (install_library).`);
110
+ const file = cacheFile(root, lib.id, version);
111
+ if (memo?.key === file) return memo.icons;
112
+ const running = inflight.get(file);
113
+ if (running) return running;
114
+ const job = (async () => {
115
+ let icons = null;
116
+ try {
117
+ icons = JSON.parse(fs.readFileSync(file, "utf8"));
118
+ } catch {
119
+ icons = await renderInWorker(root, lib.id);
120
+ fs.mkdirSync(path.dirname(file), { recursive: true });
121
+ fs.writeFileSync(file, JSON.stringify(icons));
122
+ }
123
+ memo = { key: file, icons };
124
+ return icons;
125
+ })().finally(() => inflight.delete(file));
126
+ inflight.set(file, job);
127
+ return job;
128
+ }
129
+ async function renderInWorker(root, id) {
130
+ const here = path.dirname(fileURLToPath(import.meta.url));
131
+ const worker = [path.join(here, "icons-worker.js"), path.join(here, "..", "icons-worker.js")].find((p) => fs.existsSync(p));
132
+ if (!worker) return renderIconSet(root, id);
133
+ const { Worker } = await import("worker_threads");
134
+ return new Promise((resolve, reject) => {
135
+ const w = new Worker(worker, { workerData: { root, id } });
136
+ w.once("message", (m) => m.ok ? resolve(m.icons) : reject(new Error(m.error)));
137
+ w.once("error", reject);
138
+ w.once("exit", (code) => code !== 0 && reject(new Error(`icons worker exited with ${code}`)));
139
+ });
140
+ }
141
+ function subsequence(q, name) {
142
+ let i = 0;
143
+ let start = -1;
144
+ for (let j = 0; j < name.length && i < q.length; j++) {
145
+ if (name[j] !== q[i]) continue;
146
+ if (i === 0) start = j;
147
+ i++;
148
+ if (i === q.length) return { start, span: j - start + 1 };
149
+ }
150
+ return null;
151
+ }
152
+ function distance(a, b, max = 2) {
153
+ if (Math.abs(a.length - b.length) > max) return Infinity;
154
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
155
+ for (let i = 1; i <= a.length; i++) {
156
+ const row = [i];
157
+ for (let j = 1; j <= b.length; j++) row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
158
+ if (Math.min(...row) > max) return Infinity;
159
+ prev = row;
160
+ }
161
+ return prev[b.length];
162
+ }
163
+ function searchIcons(icons, query, limit = 200) {
164
+ const q = query.trim().toLowerCase().replace(/[\s_-]+/g, "");
165
+ if (!q) return { total: icons.length, icons: icons.slice(0, limit) };
166
+ const direct = [];
167
+ const fuzzy = [];
168
+ for (const icon of icons) {
169
+ const name = icon.name.toLowerCase().replace(/^icon(?=[a-z0-9])/, "").replace(/icon$/, "");
170
+ if (name === q) direct.push({ icon, score: 0 });
171
+ else if (name.startsWith(q)) direct.push({ icon, score: 1 });
172
+ else if (q.length > 3 && distance(q, name) <= 2) direct.push({ icon, score: 1 + distance(q, name) / 4 });
173
+ else if (name.includes(q)) direct.push({ icon, score: 2 });
174
+ else if (q.length > 2) {
175
+ const m = subsequence(q, name);
176
+ if (m && m.span <= q.length * 2) fuzzy.push({ icon, score: (m.start === 0 ? 0 : 1e3) + m.span * 10 + name.length / 100 });
177
+ }
178
+ }
179
+ const byScore = (a, b) => a.score - b.score || a.icon.name.length - b.icon.name.length;
180
+ direct.sort(byScore);
181
+ fuzzy.sort(byScore);
182
+ const ranked = direct.length >= 5 ? direct : [...direct, ...fuzzy];
183
+ return { total: ranked.length, icons: ranked.slice(0, limit).map((s) => s.icon) };
184
+ }
185
+ function iconLibraries(root) {
186
+ return ICON_LIBRARIES.map((l) => ({ id: l.id, label: l.label, package: l.package, from: l.from, homepage: l.homepage, version: installedVersion(root, l.package) }));
187
+ }
188
+
189
+ export {
190
+ ICON_LIBRARIES,
191
+ iconLibrary,
192
+ renderIconSet,
193
+ loadIcons,
194
+ searchIcons,
195
+ iconLibraries
196
+ };
@@ -284,9 +284,20 @@ function importSpecifier(root, canvasFile, componentFile) {
284
284
  if (!rel.startsWith(".")) rel = `./${rel}`;
285
285
  return rel;
286
286
  }
287
+ function resolveAliasPath(root, spec) {
288
+ const alias = readAlias(root);
289
+ if (alias && spec.startsWith(alias.prefix)) return path.join(alias.dir, spec.slice(alias.prefix.length));
290
+ return path.resolve(root, spec);
291
+ }
287
292
  var aliasCache = null;
288
293
  function readAlias(root) {
289
- if (aliasCache?.root === root) return aliasCache.value;
294
+ const configFile = path.join(root, "tsconfig.json");
295
+ let key = `${root}\0none`;
296
+ try {
297
+ key = `${root}\0${fs.statSync(configFile).mtimeMs}`;
298
+ } catch {
299
+ }
300
+ if (aliasCache?.key === key) return aliasCache.value;
290
301
  let value = null;
291
302
  try {
292
303
  const ts = require2("typescript");
@@ -296,9 +307,9 @@ function readAlias(root) {
296
307
  const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath));
297
308
  const paths = parsed.options.paths ?? {};
298
309
  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)) };
310
+ for (const [k, targets] of Object.entries(paths)) {
311
+ if (k.endsWith("/*") && targets[0]?.endsWith("/*")) {
312
+ value = { prefix: k.slice(0, -1), dir: path.resolve(base, targets[0].slice(0, -2)) };
302
313
  break;
303
314
  }
304
315
  }
@@ -306,12 +317,13 @@ function readAlias(root) {
306
317
  } catch {
307
318
  value = null;
308
319
  }
309
- aliasCache = { root, value };
320
+ aliasCache = { key, value };
310
321
  return value;
311
322
  }
312
323
 
313
324
  export {
314
325
  Catalog,
315
326
  analyzeComponents,
316
- importSpecifier
327
+ importSpecifier,
328
+ resolveAliasPath
317
329
  };